4me-sdk 1.1.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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