4me-sdk 1.1.4 → 1.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 864b07c2b953a22cde7b603d2ca40e0c922af5ab
4
- data.tar.gz: 191f083e0183d044f77c9872a3b28f6e3541bf24
2
+ SHA256:
3
+ metadata.gz: dc85cfa0c6b08a248805e24e37b4fe0cde152c64d42443b72ca8db4e5cf2799a
4
+ data.tar.gz: ebfe8f4d1e32e791ef5d109ef6934aa3fa585a60df8cd1f4f000073993cc9a2f
5
5
  SHA512:
6
- metadata.gz: 161e613f6c9e5af777051f09aad8c83cd4ade36395550e0c4da63c8d7ee4a399f161aadbd0c3ee965db44864c2faa3ed4f336017b98e85b9196c6a0ee8844d13
7
- data.tar.gz: 348aa6bb7536b81c6cf0642e0b7376c3aea935d88dc6743af82cdf563b023d92f82773f72ef500c44458a448eb96b0027bce74d9e4b79f7f9c2901f3483bb013
6
+ metadata.gz: e05769acff36f8c0ffc6758d4078217b4af13cd969814ee85699d0e84951c6b9d34d6d4b2d7fb0fddb2577a2a03b4aeda3b7c687d0ec62916ef8e712ce5224cc
7
+ data.tar.gz: 6cac609bb11b34d97086e701701d7f664ca24500cca943a948e7ce42d7650e2dd2c8f14a0a348dd6d0b2295c90257d1a39411ae0f2e7a8a67202396edf8dc31b
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency 'bundler', '~> 1'
35
35
  spec.add_development_dependency 'rake', '~> 12'
36
36
  spec.add_development_dependency 'rspec', '~> 3.3'
37
- spec.add_development_dependency 'webmock', '~> 2'
37
+ spec.add_development_dependency 'webmock', '~> 3'
38
38
  spec.add_development_dependency 'simplecov', '~> 0'
39
39
 
40
40
  end
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- 4me-sdk (1.1.4)
4
+ 4me-sdk (1.2.0)
5
5
  activesupport (>= 4.2)
6
6
  gem_config (>= 0.3)
7
7
  mime-types (>= 3.0)
@@ -9,28 +9,27 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- activesupport (5.2.1)
12
+ activesupport (5.2.2)
13
13
  concurrent-ruby (~> 1.0, >= 1.0.2)
14
14
  i18n (>= 0.7, < 2)
15
15
  minitest (~> 5.1)
16
16
  tzinfo (~> 1.1)
17
- addressable (2.5.2)
18
- public_suffix (>= 2.0.2, < 4.0)
19
- concurrent-ruby (1.1.3)
20
- crack (0.4.3)
21
- safe_yaml (~> 1.0.0)
17
+ addressable (2.7.0)
18
+ public_suffix (>= 2.0.2, < 5.0)
19
+ concurrent-ruby (1.1.5)
20
+ crack (0.4.4)
22
21
  diff-lcs (1.3)
23
22
  docile (1.3.1)
24
23
  gem_config (0.3.1)
25
- hashdiff (0.3.7)
26
- i18n (1.1.1)
24
+ hashdiff (1.0.1)
25
+ i18n (1.5.1)
27
26
  concurrent-ruby (~> 1.0)
28
27
  json (2.1.0)
29
28
  mime-types (3.2.2)
30
29
  mime-types-data (~> 3.2015)
31
- mime-types-data (3.2018.0812)
32
- minitest (5.11.3)
33
- public_suffix (3.0.3)
30
+ mime-types-data (3.2019.0331)
31
+ minitest (5.13.0)
32
+ public_suffix (4.0.6)
34
33
  rake (12.3.1)
35
34
  rspec (3.8.0)
36
35
  rspec-core (~> 3.8.0)
@@ -45,7 +44,6 @@ GEM
45
44
  diff-lcs (>= 1.2.0, < 2.0)
46
45
  rspec-support (~> 3.8.0)
47
46
  rspec-support (3.8.0)
48
- safe_yaml (1.0.4)
49
47
  simplecov (0.16.1)
50
48
  docile (~> 1.1)
51
49
  json (>= 1.8, < 3)
@@ -54,10 +52,10 @@ GEM
54
52
  thread_safe (0.3.6)
55
53
  tzinfo (1.2.5)
56
54
  thread_safe (~> 0.1)
57
- webmock (2.3.2)
55
+ webmock (3.9.1)
58
56
  addressable (>= 2.3.6)
59
57
  crack (>= 0.3.2)
60
- hashdiff
58
+ hashdiff (>= 0.4.0, < 2.0.0)
61
59
 
62
60
  PLATFORMS
63
61
  ruby
@@ -68,7 +66,7 @@ DEPENDENCIES
68
66
  rake (~> 12)
69
67
  rspec (~> 3.3)
70
68
  simplecov (~> 0)
71
- webmock (~> 2)
69
+ webmock (~> 3)
72
70
 
73
71
  BUNDLED WITH
74
- 1.16.2
72
+ 1.17.3
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2018 https://code.4me.com
3
+ Copyright (c) 2019 https://code.4me.com
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sdk4me::Client
2
2
 
3
- Client for accessing the [4me REST API](http://developer.4me.com/v1/)
3
+ Client for accessing the [4me REST API](https://developer.4me.com/v1/)
4
4
 
5
5
  ## Installation
6
6
 
@@ -22,7 +22,7 @@ Or install it yourself as:
22
22
 
23
23
  ```
24
24
  Sdk4me.configure do |config|
25
- config.api_token = 'd41f5868feb65fc87fa2311a473a8766ea38bc40'
25
+ config.access_token = 'd41f5868feb65fc87fa2311a473a8766ea38bc40'
26
26
  config.account = 'my-sandbox'
27
27
  config.logger = Rails.logger
28
28
  ...
@@ -32,17 +32,20 @@ end
32
32
  All options available:
33
33
 
34
34
  * _logger_: The [Ruby Logger](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html) instance, default: `Logger.new(STDOUT)`
35
- * _host_: The [4me API host](http://developer.4me.com/v1/#service-url), default: 'https://api.4me.com'
36
- * _api_version_: The [4me API version](http://developer.4me.com/v1/#service-url), default: 'v1'
37
- * _api_token_: (**required**) The [4me API token](http://developer.4me.com/v1/#api-tokens)
38
- * _account_: Specify a [different account](http://developer.4me.com/v1/#multiple-accounts) to work with
39
- * _source_: The [source](http://developer.4me.com/v1/general/source/) used when creating new records
40
- * _max_retry_time_: maximum nr of seconds to wait for server to respond (default = 5400 = 1.5 hours)<br/>
35
+ * _host_: The [4me API host](https://developer.4me.com/v1/#service-url), default: 'https://api.4me.com'
36
+ * _api_version_: The [4me API version](https://developer.4me.com/v1/#service-url), default: 'v1'
37
+ * _access_token_: (**required**) The [4me access token](https://developer.4me.com/v1/#authentication)
38
+ * _api_token_: (**deprecated**) The [4me API token](https://developer.4me.com/v1/#api-tokens)
39
+ * _account_: Specify a [different account](https://developer.4me.com/v1/#multiple-accounts) to work with
40
+ * _source_: The [source](https://developer.4me.com/v1/general/source/) used when creating new records
41
+ * _max_retry_time_: maximum nr of seconds to retry a request on a failed response (default = 300 = 5 minutes)<br/>
41
42
  The sleep time between retries starts at 2 seconds and doubles after each retry, i.e.
42
- 2, 6, 18, 54, 162, 486, 1458, 4374, 13122, ... seconds.<br/>
43
- One retry will always be performed unless you set the value to -1.
43
+ 2, 6, 18, 54, 162, 486, 1458, ... seconds.<br/>
44
+ Set to 0 to prevent retries.
44
45
  * _read_timeout_: [HTTP read timeout](http://ruby-doc.org/stdlib-2.0.0/libdoc/net/http/rdoc/Net/HTTP.html#method-i-read_timeout-3D) in seconds (default = 25)
45
- * _block_at_rate_limit_: Set to `true` to block the request until the [rate limit](http://developer.4me.com/v1/#rate-limiting) is lifted, default: `false`
46
+ * _block_at_rate_limit_: Set to `true` to block the request until the [rate limit](https://developer.4me.com/v1/#rate-limiting) is lifted, default: `true`<br/>
47
+ The `Retry-After` header is used to compute when the retry should be performed. If that moment is later than the _max_throttle_time_ the request will not be blocked and the throttled response is returned.
48
+ * _max_throttle_time_: maximum nr of seconds to retry a request on a rate limiter (default = 3660 = 1 hour and 1 minute)<br/>
46
49
  * _proxy_host_: Define in case HTTP traffic needs to go through a proxy
47
50
  * _proxy_port_: Port of the proxy, defaults to 8080
48
51
  * _proxy_user_: Proxy user
@@ -68,7 +71,7 @@ Minimal example:
68
71
  ```
69
72
  require 'sdk4me/client'
70
73
 
71
- client = Sdk4me::Client.new(api_token: '3a4e4590179263839...')
74
+ client = Sdk4me::Client.new(access_token: '3a4e4590179263839...')
72
75
  response = client.get('me')
73
76
  puts response[:primary_email]
74
77
  ```
@@ -82,7 +85,7 @@ response = Sdk4me::Client.new.get('organizations/4321')
82
85
  puts response[:name]
83
86
  ```
84
87
 
85
- By default this call will return all [fields](http://developer.4me.com/v1/organizations/#fields) of the Organization.
88
+ By default this call will return all [fields](https://developer.4me.com/v1/organizations/#fields) of the Organization.
86
89
 
87
90
  The fields can be accessed using *symbols* and *strings*, and it is possible chain a number of keys in one go:
88
91
  ```
@@ -103,8 +106,8 @@ end
103
106
  puts "Found #{count} organizations"
104
107
  ```
105
108
 
106
- By default this call will return all [collection fields](http://developer.4me.com/v1/organizations/#collection-fields) for each Organization.
107
- For more fields, check out the [field selection](http://developer.4me.com/v1/general/field_selection/#collection-of-resources) documentation.
109
+ By default this call will return all [collection fields](https://developer.4me.com/v1/organizations/#collection-fields) for each Organization.
110
+ For more fields, check out the [field selection](https://developer.4me.com/v1/general/field_selection/#collection-of-resources) documentation.
108
111
 
109
112
  The fields can be accessed using *symbols* and *strings*, and it is possible chain a number of keys in one go:
110
113
  ```
@@ -121,7 +124,7 @@ Note that an `Sdk4me::Exception` could be thrown in case one of the API requests
121
124
 
122
125
  The `each` method [described above](#browse-through-a-collection-of-records) is the preferred way to work with collections of data.
123
126
 
124
- If you really want to [paginate](http://developer.4me.com/v1/general/pagination/) yourself, the `get` method is your friend.
127
+ If you really want to [paginate](https://developer.4me.com/v1/general/pagination/) yourself, the `get` method is your friend.
125
128
 
126
129
  ```
127
130
  @client = Sdk4me::Client.new
@@ -139,8 +142,8 @@ next_page = @client.get(response.pagination_link(:next))
139
142
  last_page = @client.get(response.pagination_link(:last))
140
143
  ```
141
144
 
142
- By default this call will return all [collection fields](http://developer.4me.com/v1/organizations/#collection-fields) for each Organization.
143
- For more fields, check out the [field selection](http://developer.4me.com/v1/general/field_selection/#collection-of-resources) documentation.
145
+ By default this call will return all [collection fields](https://developer.4me.com/v1/organizations/#collection-fields) for each Organization.
146
+ For more fields, check out the [field selection](https://developer.4me.com/v1/general/field_selection/#collection-of-resources) documentation.
144
147
 
145
148
  The fields can be accessed using *symbols* and *strings*, and it is possible chain a number of keys in one go:
146
149
  ```
@@ -211,6 +214,14 @@ response = Sdk4me::Client.new.put('requests/416621', {
211
214
  })
212
215
  ```
213
216
 
217
+ It is also possible to add inline attachments as follows:
218
+ ```
219
+ response = Sdk4me::Client.new.put('requests/416621', {
220
+ note: 'Here is some inspiration for you: [attachment:/tmp/images/puppy.png]'
221
+ })
222
+ ```
223
+ Note that only images are accepted as inline attachments.
224
+
214
225
  If an attachment upload fails, the errors are logged but the `post` or `put` request will still be sent to 4me without the
215
226
  failed attachments. To receive exceptions add `attachments_exception: true` to the data.
216
227
 
@@ -233,7 +244,7 @@ end
233
244
 
234
245
  ### Importing CSV files
235
246
 
236
- 4me also provides an [Import API](http://developer.4me.com/v1/import/). The 4me SDK Client can be used to upload files to that API.
247
+ 4me also provides an [Import API](https://developer.4me.com/v1/import/). The 4me SDK Client can be used to upload files to that API.
237
248
 
238
249
  ```
239
250
  response = Sdk4me::Client.new.import('\tmp\people.csv', 'people')
@@ -245,9 +256,9 @@ end
245
256
 
246
257
  ```
247
258
 
248
- The second argument contains the [import type](http://developer.4me.com/v1/import/#parameters).
259
+ The second argument contains the [import type](https://developer.4me.com/v1/import/#parameters).
249
260
 
250
- It is also possible to [monitor the progress](http://developer.4me.com/v1/import/#import-progress) of the import and block until the import is complete. In that case you will need to add some exception handling to your code.
261
+ It is also possible to [monitor the progress](https://developer.4me.com/v1/import/#import-progress) of the import and block until the import is complete. In that case you will need to add some exception handling to your code.
251
262
 
252
263
  ```
253
264
  begin
@@ -267,7 +278,7 @@ Note that blocking for the import to finish is required when you import multiple
267
278
 
268
279
  ### Exporting CSV files
269
280
 
270
- 4me also provides an [Export API](http://developer.4me.com/v1/export/). The 4me SDK Client can be used to download (zipped) CSV files using that API.
281
+ 4me also provides an [Export API](https://developer.4me.com/v1/export/). The 4me SDK Client can be used to download (zipped) CSV files using that API.
271
282
 
272
283
  ```
273
284
  response = Sdk4me::Client.new.export(['people', 'people_contact_details'], DateTime.new(2012,03,30,23,00,00))
@@ -279,10 +290,10 @@ end
279
290
 
280
291
  ```
281
292
 
282
- The first argument contains the [export types](http://developer.4me.com/v1/export/#parameters).
293
+ The first argument contains the [export types](https://developer.4me.com/v1/export/#parameters).
283
294
  The second argument is optional and limits the export to all changed records since the given time.
284
295
 
285
- It is also possible to [monitor the progress](http://developer.4me.com/v1/export/#export-progress) of the export and block until the export is complete. In that case you will need to add some exception handling to your code.
296
+ It is also possible to [monitor the progress](https://developer.4me.com/v1/export/#export-progress) of the export and block until the export is complete. In that case you will need to add some exception handling to your code.
286
297
 
287
298
  ```
288
299
  require 'open-uri'
@@ -304,10 +315,17 @@ Note that blocking for the export to finish is recommended as you will get direc
304
315
 
305
316
  ### Blocking
306
317
 
307
- By default all actions on the 4me SDK Client will block until the 4me API is accessible, see the _max_retry_time_ option in the [configuration](#configuration). This is especially helpfull for flaky internet connections.
318
+ When the currently used API token hits the [4me rate limiter](https://developer.4me.com/v1/#rate-limiting) a HTTP 429 response is returned that specifies after how many seconds the rate limit will be lifted.
319
+
320
+ If that time lies within the _max_throttle_time_ the 4me SDK Client will wait and retry the action, if not, the throttled response will be returned. You can verify if a response was throttled using:
321
+ ```
322
+ response = client.get('me')
323
+ puts response.throttled?
324
+ ```
308
325
 
309
- By setting the _block_at_rate_limit_ to `true` in the [configuration](#configuration) all actions will also block in case the [rate limit](http://developer.4me.com/v1/#rate-limiting) is reached. The action is retried every 5 minutes until the [rate limit](http://developer.4me.com/v1/#rate-limiting) is lifted again, which might take up to 1 hour.
326
+ By setting the _block_at_rate_limit_ to `false` in the [configuration](#configuration) the 4me SDK Client will never wait when a rate limit is hit.
310
327
 
328
+ Note that 4me has different rate limiters. If the intention is to only wait when the short-burst (max 20 requests in 2 seconds) rate limiter is hit, you could set the _max_throttle_time_ to e.g. 5 seconds.
311
329
 
312
330
  ### Translations
313
331
 
@@ -319,7 +337,7 @@ When exporting translations, the _locale_ parameter is required:
319
337
 
320
338
  ### Exception handling
321
339
 
322
- The standard methods `get`, `post`, `put` and `delete` will always return a Response with an [error message](http://developer.4me.com/v1/#http-status-codes) in case something went wrong.
340
+ The standard methods `get`, `post`, `put` and `delete` will always return a Response with an [error message](https://developer.4me.com/v1/#http-status-codes) in case something went wrong.
323
341
 
324
342
  By calling `response.valid?` you will know if the action succeeded or not, and `response.message` provides additinal information in case the response was invalid.
325
343
 
@@ -9,15 +9,16 @@ module Sdk4me
9
9
 
10
10
  has :host, classes: String, default: 'https://api.4me.com'
11
11
  has :api_version, values: ['v1'], default: 'v1'
12
+ has :access_token, classes: String
12
13
  has :api_token, classes: String
13
14
 
14
15
  has :account, classes: String
15
16
  has :source, classes: String
16
17
 
17
- has :max_retry_time, classes: Integer, default: 5400
18
+ has :max_retry_time, classes: Integer, default: 300
18
19
  has :read_timeout, classes: Integer, default: 25
19
- has :block_at_rate_limit, classes: [TrueClass, FalseClass], default: false
20
-
20
+ has :block_at_rate_limit, classes: [TrueClass, FalseClass], default: true
21
+ has :max_throttle_time, classes: Integer, default: 3660
21
22
  has :proxy_host, classes: String
22
23
  has :proxy_port, classes: Integer, default: 8080
23
24
  has :proxy_user, classes: String
@@ -27,7 +27,7 @@ module Sdk4me
27
27
  #
28
28
  # Shared configuration for all 4me SDK Clients:
29
29
  # Sdk4me.configure do |config|
30
- # config.api_token = 'd41f5868feb65fc87fa2311a473a8766ea38bc40'
30
+ # config.access_token = 'd41f5868feb65fc87fa2311a473a8766ea38bc40'
31
31
  # config.account = 'my-sandbox'
32
32
  # ...
33
33
  # end
@@ -39,11 +39,11 @@ module Sdk4me
39
39
  # - logger: The Ruby Logger instance, default: Logger.new(STDOUT)
40
40
  # - host: The 4me API host, default: 'https://api.4me.com'
41
41
  # - api_version: The 4me API version, default: 'v1'
42
- # - api_token: *required* The 4me API token
42
+ # - access_token: *required* The 4me access token
43
43
  # - account: Specify a different (trusted) account to work with
44
- # @see http://developer.4me.com/v1/#multiple-accounts
44
+ # @see https://developer.4me.com/v1/#multiple-accounts
45
45
  # - source: The Source used when creating new records
46
- # @see http://developer.4me.com/v1/general/source/
46
+ # @see https://developer.4me.com/v1/general/source/
47
47
  #
48
48
  # - max_retry_time: maximum nr of seconds to wait for server to respond (default = 5400 = 1.5 hours)
49
49
  # the sleep time between retries starts at 2 seconds and doubles after each retry
@@ -51,7 +51,7 @@ module Sdk4me
51
51
  # one retry will always be performed unless you set the value to -1
52
52
  # - read_timeout: HTTP GET read timeout in seconds (default = 25)
53
53
  # - block_at_rate_limit: Set to +true+ to block the request until the rate limit is lifted, default: +false+
54
- # @see http://developer.4me.com/v1/#rate-limiting
54
+ # @see https://developer.4me.com/v1/#rate-limiting
55
55
  #
56
56
  # - proxy_host: Define in case HTTP traffic needs to go through a proxy
57
57
  # - proxy_port: Port of the proxy, defaults to 8080
@@ -59,12 +59,19 @@ module Sdk4me
59
59
  # - proxy_password: Proxy password
60
60
  def initialize(options = {})
61
61
  @options = Sdk4me.configuration.current.merge(options)
62
- [:host, :api_version, :api_token].each do |required_option|
62
+ [:host, :api_version].each do |required_option|
63
63
  raise ::Sdk4me::Exception.new("Missing required configuration option #{required_option}") if option(required_option).blank?
64
64
  end
65
+ @logger = @options[:logger]
65
66
  @ssl, @domain, @port = ssl_domain_port_path(option(:host))
67
+ unless option(:access_token).present?
68
+ if option(:api_token).blank?
69
+ raise ::Sdk4me::Exception.new("Missing required configuration option access_token")
70
+ else
71
+ @logger.info('Use of api_token is deprecated, consider switching to access_token instead.')
72
+ end
73
+ end
66
74
  @ssl_verify_none = options[:ssl_verify_none]
67
- @logger = @options[:logger]
68
75
  end
69
76
 
70
77
  # Retrieve an option
@@ -211,7 +218,12 @@ module Sdk4me
211
218
  def expand_header(header = {})
212
219
  header = DEFAULT_HEADER.merge(header)
213
220
  header['X-4me-Account'] = option(:account) if option(:account)
214
- header['AUTHORIZATION'] = 'Basic ' + ["#{option(:api_token)}:x"].pack('m*').gsub(/\s/, '')
221
+ if option(:access_token).present?
222
+ header['AUTHORIZATION'] = 'Bearer ' + option(:access_token)
223
+ else
224
+ token_and_password = option(:api_token).include?(':') ? option(:api_token) : "#{option(:api_token)}:x"
225
+ header['AUTHORIZATION'] = 'Basic ' + [token_and_password].pack('m*').gsub(/\s/, '')
226
+ end
215
227
  if option(:source)
216
228
  header['X-4me-Source'] = option(:source)
217
229
  header['HTTP_USER_AGENT'] = option(:source)
@@ -283,7 +295,7 @@ module Sdk4me
283
295
  elsif '303' == response.raw.code.to_s
284
296
  @logger.debug { "Redirect: #{response.raw.header['Location']}" }
285
297
  else
286
- @logger.error { "Request failed: #{response.message}" }
298
+ @logger.error { "#{request.method} request to #{domain}:#{port}#{request.path} failed: #{response.message}" }
287
299
  end
288
300
  response
289
301
  end
@@ -298,14 +310,25 @@ module Sdk4me
298
310
  end
299
311
 
300
312
  module SendWithRateLimitBlock
301
- # Wraps the _send method with retries when the server does not responsd, see +initialize+ option +:rate_limit_block+
313
+ # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:rate_limit_block+
302
314
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
303
- return super(request, domain, port, ssl) unless option(:block_at_rate_limit)
304
- now = Time.now
315
+ return super(request, domain, port, ssl) unless option(:block_at_rate_limit) && option(:max_throttle_time) > 0
316
+ now = nil
317
+ timed_out = false
305
318
  begin
306
319
  _response = super(request, domain, port, ssl)
307
- @logger.warn { "Request throttled, trying again in 5 minutes: #{_response.message}" } and sleep(300) if _response.throttled?
308
- end while _response.throttled? && (Time.now - now) < 3660 # max 1 hour and 1 minute
320
+ now ||= Time.now
321
+ if _response.throttled?
322
+ # if no Retry-After is not provided, the 4me server is very busy, wait 5 minutes
323
+ retry_after = _response.retry_after == 0 ? 300 : [_response.retry_after, 2].max
324
+ if (Time.now - now + retry_after) < option(:max_throttle_time)
325
+ @logger.warn { "Request throttled, trying again in #{retry_after} seconds: #{_response.message}" }
326
+ sleep(retry_after)
327
+ else
328
+ timed_out = true
329
+ end
330
+ end
331
+ end while _response.throttled? && !timed_out
309
332
  _response
310
333
  end
311
334
  end
@@ -314,15 +337,24 @@ module Sdk4me
314
337
  module SendWithRetries
315
338
  # Wraps the _send method with retries when the server does not respond, see +initialize+ option +:retries+
316
339
  def _send(request, domain = @domain, port = @port, ssl = @ssl)
340
+ return super(request, domain, port, ssl) unless option(:max_retry_time) > 0
317
341
  retries = 0
318
- sleep_time = 2
319
- total_retry_time = 0
342
+ sleep_time = 1
343
+ now = nil
344
+ timed_out = false
320
345
  begin
321
346
  _response = super(request, domain, port, ssl)
322
- @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{_response.message}" } and sleep(sleep_time) if (_response.raw.code.to_s != '204' && _response.empty?) && option(:max_retry_time) > 0
323
- total_retry_time += sleep_time
324
- sleep_time *= 2
325
- end while (_response.raw.code.to_s != '204' && _response.empty?) && total_retry_time < option(:max_retry_time)
347
+ now ||= Time.now
348
+ if _response.failure?
349
+ sleep_time *= 2
350
+ if (Time.now - now + sleep_time) < option(:max_retry_time)
351
+ @logger.warn { "Request failed, retry ##{retries += 1} in #{sleep_time} seconds: #{_response.message}" }
352
+ sleep(sleep_time)
353
+ else
354
+ timed_out = true
355
+ end
356
+ end
357
+ end while _response.failure? && !timed_out
326
358
  _response
327
359
  end
328
360
  end
@@ -8,21 +8,64 @@ module Sdk4me
8
8
  @client = client
9
9
  end
10
10
 
11
- # upload the attachments in :attachments to 4me and return the data with the uploaded attachment info
11
+ # upload the attachments and return the data with the uploaded attachment info
12
+ # Two flavours available
13
+ # * data[:attachments]
14
+ # * data[:note] containing text with '[attachment:/tmp/images/green_fuzz.jpg]'
12
15
  def upload_attachments!(path, data)
13
- raise_exceptions = !!data.delete(:attachments_exception)
16
+ upload_options = {
17
+ raise_exceptions: !!data.delete(:attachments_exception),
18
+ attachments_field: attachments_field(path),
19
+ }
20
+ uploaded_attachments = upload_normal_attachments!(path, data, upload_options)
21
+ uploaded_attachments += upload_inline_attachments!(path, data, upload_options)
22
+ # jsonify the attachments, if any were uploaded
23
+ data[upload_options[:attachments_field]] = uploaded_attachments.compact.to_json if uploaded_attachments.compact.any?
24
+ end
25
+
26
+ private
27
+
28
+ # upload the attachments in :attachments to 4me and return the data with the uploaded attachment info
29
+ def upload_normal_attachments!(path, data, upload_options)
14
30
  attachments = [data.delete(:attachments)].flatten.compact
15
- return if attachments.empty?
31
+ return [] if attachments.empty?
16
32
 
17
- # retrieve the upload configuration for this record from 4me
18
- storage = @client.get(path =~ /\d+$/ ? path : "#{path}/new", {attachment_upload_token: true}, @client.send(:expand_header))[:storage_upload]
19
- report_error("Attachments not allowed for #{path}", raise_exceptions) and return unless storage
33
+ upload_options[:storage] ||= storage(path, upload_options[:raise_exceptions])
34
+ return [] unless upload_options[:storage]
20
35
 
21
- # upload each attachment and store the {key, filesize} has in the note_attachments parameter
22
- data[attachments_field(path)] = attachments.map {|attachment| upload_attachment(storage, attachment, raise_exceptions) }.compact.to_json
36
+ attachments.map do |attachment|
37
+ upload_attachment(upload_options[:storage], attachment, upload_options[:raise_exceptions])
38
+ end
23
39
  end
24
40
 
25
- private
41
+ INLINE_ATTACHMENT_REGEXP = /\[attachment:([^\]]+)\]/.freeze
42
+ # upload any '[attachment:/tmp/images/green_fuzz.jpg]' in :note text field to 4me as inline attachment and add the s3 key to the text
43
+ def upload_inline_attachments!(path, data, upload_options)
44
+ text_field = upload_options[:attachments_field].to_s.gsub('_attachments', '').to_sym
45
+ return [] unless (data[text_field] || '') =~ INLINE_ATTACHMENT_REGEXP
46
+
47
+ upload_options[:storage] ||= storage(path, upload_options[:raise_exceptions])
48
+ return [] unless upload_options[:storage]
49
+
50
+ attachments = []
51
+ data[text_field] = data[text_field].gsub(INLINE_ATTACHMENT_REGEXP) do |full_match|
52
+ attachment_details = upload_attachment(upload_options[:storage], $~[1], upload_options[:raise_exceptions])
53
+ if attachment_details
54
+ attachments << attachment_details.merge(inline: true)
55
+ "![](#{attachment_details[:key]})" # magic markdown for inline attachments
56
+ else
57
+ full_match
58
+ end
59
+ end
60
+ attachments
61
+ end
62
+
63
+ def storage(path, raise_exceptions)
64
+ # retrieve the upload configuration for this record from 4me
65
+ storage = @client.get(path =~ /\d+$/ ? path : "#{path}/new", {attachment_upload_token: true}, @client.send(:expand_header))[:storage_upload]
66
+ report_error("Attachments not allowed for #{path}", raise_exceptions) unless storage
67
+ storage
68
+ end
26
69
 
27
70
  def attachments_field(path)
28
71
  case path