erp_integration 0.55.0 → 0.57.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 +4 -4
- data/.ruby-version +1 -1
- data/README.md +3 -0
- data/lib/erp_integration/configuration.rb +19 -5
- data/lib/erp_integration/fulfil/client.rb +11 -2
- data/lib/erp_integration/logger.rb +29 -0
- data/lib/erp_integration/middleware/api_keys_rotation.rb +9 -2
- data/lib/erp_integration/middleware/formatter.rb +38 -0
- data/lib/erp_integration/middleware/logger.rb +7 -9
- data/lib/erp_integration/rate_limiter.rb +21 -2
- data/lib/erp_integration/version.rb +1 -1
- data/lib/erp_integration.rb +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d3ca1dee9c59a3fe44512bb878c063385b9ce338a8fdc4a6ff5b9c8f7167609d
|
4
|
+
data.tar.gz: cd1a513db316b7a69ace5ef024f4e64bc8214d4a08fc19c939280bb8217f505b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1c33b241d2bf37cdc487e49079d0fe85500125216799b872ea2921baf5769dbb33c0c57e9679cf09a5aa8ea8b68e0738052275c5b58883a76a12d4221431d19
|
7
|
+
data.tar.gz: 622bc6cd1456950c07a1b67ff75ab8c66e5c7e6ece19dc685d592e43adad88ce3aaa0b927e456ecc1344a3e5bebbaf81d529755c644ee4249459aa2e4701fb84
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.
|
1
|
+
3.2.2
|
data/README.md
CHANGED
@@ -44,10 +44,13 @@ end
|
|
44
44
|
To set up API key rotation, configure multiple keys.
|
45
45
|
Every time a Fulfil client receives a `404` or `429`, it will rotate an API key.
|
46
46
|
|
47
|
+
You can set the `api_key_rotation_threshold` option to automatically change the API key when requests are running low. If you don't want to use the `x-ratelimit-remaining` response header for rotation, set the `api_key_rotation_threshold` to 0 or ignore it. The minimum threshold is 1, meaning the key will rotate when there are 0 remaining requests.
|
48
|
+
|
47
49
|
```erb
|
48
50
|
# config/initializers/erp_integration.rb
|
49
51
|
ErpIntegration.configure do |config|
|
50
52
|
config.fulfil_api_keys = ['<your-api-key1>', '<your-api-key2>']
|
53
|
+
config.api_key_rotation_threshold = 1
|
51
54
|
...
|
52
55
|
end
|
53
56
|
```
|
@@ -22,6 +22,12 @@ module ErpIntegration
|
|
22
22
|
# @return [String] The base URL for Fulfil.
|
23
23
|
attr_accessor :fulfil_base_url
|
24
24
|
|
25
|
+
# The `api_key_rotation_threshold` is used by the `ApiKeysRotation` middleware
|
26
|
+
# it will rotate the API key when the remaining requests are less than the threshold.
|
27
|
+
#
|
28
|
+
# Set the `api_key_rotation_threshold` to 0, it you don't want to rotate by the remaining requests.
|
29
|
+
attr_reader :api_key_rotation_threshold
|
30
|
+
|
25
31
|
# Allows configuring an adapter for the `BillOfMaterial` resource. When
|
26
32
|
# none is configured, it will default to Fulfil.
|
27
33
|
# @return [Symbol] The configured adapter for the bill_of_material.
|
@@ -162,16 +168,16 @@ module ErpIntegration
|
|
162
168
|
# @return [Symbol] The configured adapter for the tracking number.
|
163
169
|
attr_writer :product_option_adapter
|
164
170
|
|
165
|
-
# Logger that will be used for HTTP operations on Client
|
166
|
-
# @return [Logger] The configured logger
|
167
|
-
attr_accessor :logger
|
168
|
-
|
169
171
|
def initialize(**options)
|
170
172
|
options.each_pair do |key, value|
|
171
173
|
public_send("#{key}=", value) if respond_to?("#{key}=")
|
172
174
|
end
|
173
175
|
end
|
174
176
|
|
177
|
+
def api_key_rotation_threshold=(threshold)
|
178
|
+
@api_key_rotation_threshold = threshold || 0
|
179
|
+
end
|
180
|
+
|
175
181
|
def carrier_adapter
|
176
182
|
@carrier_adapter || :fulfil
|
177
183
|
end
|
@@ -309,7 +315,15 @@ module ErpIntegration
|
|
309
315
|
# Sets the rate limiters that will be used
|
310
316
|
# @raise [MissingMethodError] if the rate limiter object doesn't respond to the required methods.
|
311
317
|
def rate_limiters=(rate_limiters)
|
312
|
-
@rate_limiters = rate_limiters.each { |limiter| RateLimiter.validate!(limiter) }
|
318
|
+
@rate_limiters = (rate_limiters || []).each { |limiter| RateLimiter.validate!(limiter) }
|
319
|
+
end
|
320
|
+
|
321
|
+
def logger
|
322
|
+
@logger || Logger.new(nil)
|
323
|
+
end
|
324
|
+
|
325
|
+
def logger=(logger)
|
326
|
+
@logger = Logger.new(logger)
|
313
327
|
end
|
314
328
|
end
|
315
329
|
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module ErpIntegration
|
4
4
|
module Fulfil
|
5
5
|
class Client
|
6
|
+
FORMATTER = ErpIntegration::Middleware::Formatter
|
7
|
+
|
6
8
|
attr_reader :api_keys_pool, :base_url
|
7
9
|
attr_writer :connection, :faraday_adapter, :rotate_statuses
|
8
10
|
|
@@ -33,9 +35,9 @@ module ErpIntegration
|
|
33
35
|
# Custom error handling for the error response
|
34
36
|
faraday.use ErpIntegration::Middleware::ErrorHandling
|
35
37
|
# Custom middleware for rotating API keys
|
36
|
-
faraday.use ErpIntegration::Middleware::ApiKeysRotation,
|
38
|
+
faraday.use ErpIntegration::Middleware::ApiKeysRotation, rotation_options
|
37
39
|
# Custom middleware for logging requests and responses
|
38
|
-
faraday.use ErpIntegration::Middleware::Logger, @logger if @logger
|
40
|
+
faraday.use ErpIntegration::Middleware::Logger, @logger, formatter: FORMATTER if @logger
|
39
41
|
|
40
42
|
# Adapter definition should be last in order to make the json parsers be loaded correctly
|
41
43
|
faraday.adapter faraday_adapter
|
@@ -76,6 +78,13 @@ module ErpIntegration
|
|
76
78
|
'Content-Type': 'application/json'
|
77
79
|
}
|
78
80
|
end
|
81
|
+
|
82
|
+
def rotation_options
|
83
|
+
{
|
84
|
+
api_keys_pool: api_keys_pool,
|
85
|
+
rotation_theshold: ErpIntegration.config.api_key_rotation_threshold
|
86
|
+
}
|
87
|
+
end
|
79
88
|
end
|
80
89
|
end
|
81
90
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ErpIntegration
|
4
|
+
# The Logger class is a simple wrapper around the logger object.
|
5
|
+
class Logger
|
6
|
+
delegate :debug, :info, :warn, :error, :fatal, to: :logger, allow_nil: true
|
7
|
+
|
8
|
+
def initialize(logger = nil)
|
9
|
+
@logger = logger
|
10
|
+
end
|
11
|
+
|
12
|
+
# Logs the given tags if the logger is present.
|
13
|
+
# If the logger does not respond to `tagged`, it logs the tags as an info message.
|
14
|
+
def with_tags(*tags)
|
15
|
+
return yield unless logger
|
16
|
+
|
17
|
+
if logger.respond_to?(:tagged)
|
18
|
+
logger.tagged(*tags) { yield }
|
19
|
+
else
|
20
|
+
logger.info("Requested ERP with #{tags.join(', ')}")
|
21
|
+
yield
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :logger
|
28
|
+
end
|
29
|
+
end
|
@@ -20,14 +20,21 @@ module ErpIntegration
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def on_request(env)
|
23
|
-
env
|
23
|
+
env.request_headers['X-API-KEY'] = options[:api_keys_pool].current_key
|
24
24
|
end
|
25
25
|
|
26
26
|
def on_complete(env)
|
27
|
-
return unless
|
27
|
+
return unless rotate?(env)
|
28
28
|
|
29
29
|
options[:api_keys_pool].rotate_key!
|
30
30
|
end
|
31
|
+
|
32
|
+
def rotate?(env)
|
33
|
+
return true if HTTP_ROTATE_CODES.include?(env.status)
|
34
|
+
return false unless env.response_headers['x-ratelimit-remaining'].present?
|
35
|
+
|
36
|
+
env.response_headers['x-ratelimit-remaining'].to_i < options[:rotation_theshold].to_i
|
37
|
+
end
|
31
38
|
end
|
32
39
|
end
|
33
40
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ErpIntegration
|
4
|
+
module Middleware
|
5
|
+
# The {Formatter} class is a custom formatter for the Faraday logger.
|
6
|
+
# It logs the rate limiter headers in the response.
|
7
|
+
class Formatter < Faraday::Logging::Formatter
|
8
|
+
def response(env)
|
9
|
+
super
|
10
|
+
public_send(log_level, 'response', &rate_limiter_headers(env.response_headers))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Formats the rate limiter headers.
|
14
|
+
# @param headers [Hash]
|
15
|
+
def rate_limiter_headers(headers)
|
16
|
+
proc do
|
17
|
+
[
|
18
|
+
limit(headers),
|
19
|
+
remaining(headers),
|
20
|
+
reset_at(headers)
|
21
|
+
].join(', ')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def limit(headers)
|
26
|
+
"Rate Limit #{headers['x-ratelimit-limit']}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def remaining(headers)
|
30
|
+
"Remaining Limit #{headers['x-ratelimit-remaining']}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset_at(headers)
|
34
|
+
"Rate Limit Reset at #{Time.at(headers['x-ratelimit-reset'].to_i).utc}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -19,16 +19,10 @@ module ErpIntegration
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def call(env)
|
22
|
-
api_key_fragment = sanitize_api_key(env
|
22
|
+
api_key_fragment = sanitize_api_key(env.request_headers['X-API-KEY']) || 'none'
|
23
|
+
request_uuid = SecureRandom.uuid
|
23
24
|
|
24
|
-
|
25
|
-
@logger.tagged("API key *#{api_key_fragment}") do
|
26
|
-
@formatter.request(env)
|
27
|
-
|
28
|
-
@app.call(env).on_complete { |response| on_complete(response) }
|
29
|
-
end
|
30
|
-
else
|
31
|
-
@logger.info("Requested ERP with API key *#{api_key_fragment}")
|
25
|
+
@logger.with_tags(api_key_tag(api_key_fragment), request_uuid) do
|
32
26
|
@formatter.request(env)
|
33
27
|
|
34
28
|
@app.call(env).on_complete { |response| on_complete(response) }
|
@@ -48,6 +42,10 @@ module ErpIntegration
|
|
48
42
|
def sanitize_api_key(api_key)
|
49
43
|
api_key[-4..-1] if api_key
|
50
44
|
end
|
45
|
+
|
46
|
+
def api_key_tag(api_key_fragment)
|
47
|
+
"API key *#{api_key_fragment}"
|
48
|
+
end
|
51
49
|
end
|
52
50
|
end
|
53
51
|
end
|
@@ -7,7 +7,7 @@ module ErpIntegration
|
|
7
7
|
# the rate limiter by the API key.
|
8
8
|
#
|
9
9
|
# The `within_limit` method should yield to the block if the rate limit is not exceeded.
|
10
|
-
REQUIRED_METHODS = %i[api_key_fragment within_limit].freeze
|
10
|
+
REQUIRED_METHODS = %i[api_key_fragment name within_limit].freeze
|
11
11
|
|
12
12
|
class << self
|
13
13
|
# Validates that the rate limiter object responds to the required methods.
|
@@ -29,7 +29,7 @@ module ErpIntegration
|
|
29
29
|
# @param api_key [String]
|
30
30
|
# @return [Object] The rate limiter object.
|
31
31
|
def find_by_api_key(api_key)
|
32
|
-
rate_limiters[api_key] || unlimited
|
32
|
+
new(rate_limiters[api_key] || unlimited)
|
33
33
|
end
|
34
34
|
|
35
35
|
# Returns an unlimited rate limiter.
|
@@ -53,11 +53,30 @@ module ErpIntegration
|
|
53
53
|
end
|
54
54
|
end
|
55
55
|
|
56
|
+
delegate :name, to: :@rate_limiter
|
57
|
+
|
58
|
+
def initialize(rate_limiter)
|
59
|
+
@rate_limiter = rate_limiter
|
60
|
+
end
|
61
|
+
|
62
|
+
def within_limit(&block)
|
63
|
+
ErpIntegration.config.logger.with_tags(@rate_limiter.name) do
|
64
|
+
@rate_limiter.within_limit(&block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
56
68
|
# {Unlimited} is a rate limiter that allows all requests.
|
57
69
|
# When the rate limiter wasn't found by the API key, it defaults to this class.
|
58
70
|
# Or if the `rate_limiters` option is not set in the configuration.
|
59
71
|
class Unlimited
|
60
72
|
# Executes the block without any restrictions.
|
73
|
+
|
74
|
+
attr_reader :name
|
75
|
+
|
76
|
+
def initialize
|
77
|
+
@name = 'unlimited'
|
78
|
+
end
|
79
|
+
|
61
80
|
def within_limit
|
62
81
|
yield
|
63
82
|
end
|
data/lib/erp_integration.rb
CHANGED
@@ -12,12 +12,14 @@ require_relative 'erp_integration/version'
|
|
12
12
|
require_relative 'erp_integration/errors'
|
13
13
|
require_relative 'erp_integration/api_keys_pool'
|
14
14
|
require_relative 'erp_integration/rate_limiter'
|
15
|
+
require_relative 'erp_integration/logger'
|
15
16
|
require_relative 'erp_integration/configuration'
|
16
17
|
|
17
18
|
# Middleware
|
18
19
|
require_relative 'erp_integration/middleware/api_keys_rotation'
|
19
20
|
require_relative 'erp_integration/middleware/error_handling'
|
20
21
|
require_relative 'erp_integration/middleware/logger'
|
22
|
+
require_relative 'erp_integration/middleware/formatter'
|
21
23
|
|
22
24
|
# HTTP clients
|
23
25
|
require_relative 'erp_integration/fulfil/client'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: erp_integration
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.57.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Vermaas
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-11-
|
11
|
+
date: 2024-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -317,8 +317,10 @@ files:
|
|
317
317
|
- lib/erp_integration/gift_card.rb
|
318
318
|
- lib/erp_integration/internal_shipment.rb
|
319
319
|
- lib/erp_integration/location.rb
|
320
|
+
- lib/erp_integration/logger.rb
|
320
321
|
- lib/erp_integration/middleware/api_keys_rotation.rb
|
321
322
|
- lib/erp_integration/middleware/error_handling.rb
|
323
|
+
- lib/erp_integration/middleware/formatter.rb
|
322
324
|
- lib/erp_integration/middleware/logger.rb
|
323
325
|
- lib/erp_integration/party_address.rb
|
324
326
|
- lib/erp_integration/product.rb
|
@@ -369,7 +371,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
369
371
|
- !ruby/object:Gem::Version
|
370
372
|
version: '0'
|
371
373
|
requirements: []
|
372
|
-
rubygems_version: 3.
|
374
|
+
rubygems_version: 3.5.6
|
373
375
|
signing_key:
|
374
376
|
specification_version: 4
|
375
377
|
summary: Connects Mejuri with third-party ERP vendors
|