erp_integration 0.54.0 → 0.56.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
2
  SHA256:
3
- metadata.gz: 015dee8d29c0a8bcd763966f25c24b77efb9cd2e7ac4a1555aa104e9a708c2ec
4
- data.tar.gz: 92f5a27785677904bd7b0cd60fc5a033195ed99d64d9c16941fbb4d942db8b31
3
+ metadata.gz: 55974d4e2c7b76ca461070fe996522e0b7fa06391760549f8959000ed07b2a9e
4
+ data.tar.gz: 43683efd68525709be0fcba1537051830f52d7c0663779a7ab5b4c24b0a6e2e9
5
5
  SHA512:
6
- metadata.gz: 526e86fb610c85d0b5c69f36e2cf1b1c396ff36e6f74c2db6d9764eae9eb1f076d9ed7771b5347ac3060bbdbd047777c1901c427351a280bcb26bf49b6edb269
7
- data.tar.gz: f876d9874077923090353f7e2c9d6766cae725fe1558345ba98f4d5cfc07495bb272f451205f04c6a957402fdf10bc756d69ad8792a55405c91a07ce856d5e4e
6
+ metadata.gz: f0943cc3b1047ef17259a1eeb039fdd65ee74cb3ebc6911ff431162f60058b89d8e26e3f742028b261c2979c304aab4b1a0fd155e5d72fd0f0063a3a3afa351f
7
+ data.tar.gz: 4a06481fb232e9ad506c0d038af4114169546d622eecec0e6d5b03877e6f85648777c278728f87f6cae52c3e695f382ec34e4c1a2adcfadf0d9af23fc354374f
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.0.2
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
  ```
@@ -61,6 +64,47 @@ ErpIntegration::SalesOrder.api_keys_pool = '<your-api-key>'
61
64
  ErpIntegration::SalesOrder.api_keys_pool = ['<your-api-key1>', '<your-api-key2>']
62
65
  ```
63
66
 
67
+ ### Rate limiting
68
+
69
+ To manage the number of requests made to avoid rate limiting by the API provider, you can configure rate limiters in the ERP Integration gem. Each rate limiter should implement the `api_key_fragment` and `within_limit` methods.
70
+
71
+ ### Setting Up Rate Limiters
72
+ You can configure multiple rate limiters to be used for HTTP operations on the client.
73
+
74
+ ```ruby
75
+ # config/initializers/erp_integration.rb
76
+ ErpIntegration.configure do |config|
77
+ config.rate_limiters = [
78
+ MyRateLimiter.new(api_key_fragment: 'key1'),
79
+ MyRateLimiter.new(api_key_fragment: 'key2')
80
+ ]
81
+ end
82
+ ```
83
+
84
+ Each rate limiter must implement the following methods:
85
+ - `api_key_fragment`: This method should return a string that is used to identify the rate limiter by the API key.
86
+ - `within_limit`: This method should `yield` to the block if the rate limit is not exceeded.
87
+
88
+ ### Example Rate Limiter
89
+ Here is an example implementation of a rate limiter:
90
+
91
+ ```ruby
92
+ class MyRateLimiter
93
+ def initialize(api_key_fragment:)
94
+ @api_key_fragment = api_key_fragment
95
+ end
96
+
97
+ def api_key_fragment
98
+ @api_key_fragment
99
+ end
100
+
101
+ def within_limit
102
+ # Implement your rate limiting logic here
103
+ yield
104
+ end
105
+ end
106
+ ```
107
+
64
108
  ### Supported Query Methods
65
109
 
66
110
  After configuring the gem, one can easily query all the available ERP resources from the connected third-parties. In all cases, the API will return a collection of resources.
@@ -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.
@@ -172,6 +178,10 @@ module ErpIntegration
172
178
  end
173
179
  end
174
180
 
181
+ def api_key_rotation_threshold=(threshold)
182
+ @api_key_rotation_threshold = threshold || 0
183
+ end
184
+
175
185
  def carrier_adapter
176
186
  @carrier_adapter || :fulfil
177
187
  end
@@ -299,6 +309,18 @@ module ErpIntegration
299
309
  def product_option_adapter
300
310
  @product_option_adapter || :fulfil
301
311
  end
312
+
313
+ # Rate limiters that will be used for HTTP operations on Client
314
+ # to manage the number of requests made to avoid rate limiting by the API provider.
315
+ def rate_limiters
316
+ @rate_limiters ||= []
317
+ end
318
+
319
+ # Sets the rate limiters that will be used
320
+ # @raise [MissingMethodError] if the rate limiter object doesn't respond to the required methods.
321
+ def rate_limiters=(rate_limiters)
322
+ @rate_limiters = (rate_limiters || []).each { |limiter| RateLimiter.validate!(limiter) }
323
+ end
302
324
  end
303
325
 
304
326
  # Returns ERP Integration's configuration.
@@ -33,7 +33,7 @@ module ErpIntegration
33
33
  # Custom error handling for the error response
34
34
  faraday.use ErpIntegration::Middleware::ErrorHandling
35
35
  # Custom middleware for rotating API keys
36
- faraday.use ErpIntegration::Middleware::ApiKeysRotation, api_keys_pool: api_keys_pool
36
+ faraday.use ErpIntegration::Middleware::ApiKeysRotation, rotation_options
37
37
  # Custom middleware for logging requests and responses
38
38
  faraday.use ErpIntegration::Middleware::Logger, @logger if @logger
39
39
 
@@ -48,7 +48,9 @@ module ErpIntegration
48
48
  raise ErpIntegration::Error, 'The Fulfil API key and/or base URL are missing.'
49
49
  end
50
50
 
51
- connection.public_send(action_name, "api/#{version}/#{path}", options).body
51
+ rate_limiter.within_limit do
52
+ connection.public_send(action_name, "api/#{version}/#{path}", options).body
53
+ end
52
54
  end
53
55
  end
54
56
 
@@ -64,12 +66,23 @@ module ErpIntegration
64
66
  api_keys_pool.current_key
65
67
  end
66
68
 
69
+ def rate_limiter
70
+ RateLimiter.find_by_api_key(api_key)
71
+ end
72
+
67
73
  def default_headers
68
74
  {
69
75
  'Accept': 'application/json',
70
76
  'Content-Type': 'application/json'
71
77
  }
72
78
  end
79
+
80
+ def rotation_options
81
+ {
82
+ api_keys_pool: api_keys_pool,
83
+ rotation_theshold: ErpIntegration.config.api_key_rotation_threshold
84
+ }
85
+ end
73
86
  end
74
87
  end
75
88
  end
@@ -20,14 +20,21 @@ module ErpIntegration
20
20
  end
21
21
 
22
22
  def on_request(env)
23
- env[:request_headers]['X-API-KEY'] = options[:api_keys_pool].current_key
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 HTTP_ROTATE_CODES.include?(env.status)
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
@@ -19,7 +19,7 @@ module ErpIntegration
19
19
  end
20
20
 
21
21
  def call(env)
22
- api_key_fragment = sanitize_api_key(env[:request_headers]['X-API-KEY'])
22
+ api_key_fragment = sanitize_api_key(env.request_headers['X-API-KEY']) || 'none'
23
23
 
24
24
  if @logger.respond_to?(:tagged)
25
25
  @logger.tagged("API key *#{api_key_fragment}") do
@@ -43,6 +43,8 @@ module ErpIntegration
43
43
  @formatter.exception(exc) if @formatter.respond_to?(:exception)
44
44
  end
45
45
 
46
+ # @param [String] api_key
47
+ # @return [String] The last 4 characters of the API key
46
48
  def sanitize_api_key(api_key)
47
49
  api_key[-4..-1] if api_key
48
50
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErpIntegration
4
+ class RateLimiter
5
+ class MissingMethodError < StandardError; end
6
+ # The `api_key_fragment` method should return a string that is used to identify
7
+ # the rate limiter by the API key.
8
+ #
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
11
+
12
+ class << self
13
+ # Validates that the rate limiter object responds to the required methods.
14
+ # It requires the `api_key_fragment` and `within_limit` methods to be present.
15
+ #
16
+ # @raise [MissingMethodError]
17
+ # @param rate_limiter [Object]
18
+ def validate!(rate_limiter)
19
+ REQUIRED_METHODS.each do |method|
20
+ next if rate_limiter.respond_to?(method)
21
+
22
+ raise MissingMethodError, "'#{rate_limiter.class}##{method}' method is required."
23
+ end
24
+ end
25
+
26
+ # Finds the rate limiter by the API key.
27
+ # If the API key is not found, it returns an unlimited rate limiter.
28
+ #
29
+ # @param api_key [String]
30
+ # @return [Object] The rate limiter object.
31
+ def find_by_api_key(api_key)
32
+ rate_limiters[api_key] || unlimited
33
+ end
34
+
35
+ # Returns an unlimited rate limiter.
36
+ #
37
+ # @return [RateLimiter::Unlimited] The unlimited rate limiter object.
38
+ def unlimited
39
+ @unlimited ||= Unlimited.new
40
+ end
41
+
42
+ private
43
+
44
+ # The `rate_limiters` hash stores the rate limiter objects found by the API key.
45
+ #
46
+ # @return [Hash] The rate limiters hash.
47
+ def rate_limiters
48
+ @rate_limiters ||= Hash.new do |h, api_key|
49
+ h[api_key] = ErpIntegration.config.rate_limiters.find do |rate_limiter|
50
+ api_key.end_with?(rate_limiter.api_key_fragment)
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # {Unlimited} is a rate limiter that allows all requests.
57
+ # When the rate limiter wasn't found by the API key, it defaults to this class.
58
+ # Or if the `rate_limiters` option is not set in the configuration.
59
+ class Unlimited
60
+ # Executes the block without any restrictions.
61
+ def within_limit
62
+ yield
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ErpIntegration
4
- VERSION = '0.54.0'
4
+ VERSION = '0.56.0'
5
5
  end
@@ -11,6 +11,7 @@ require 'json'
11
11
  require_relative 'erp_integration/version'
12
12
  require_relative 'erp_integration/errors'
13
13
  require_relative 'erp_integration/api_keys_pool'
14
+ require_relative 'erp_integration/rate_limiter'
14
15
  require_relative 'erp_integration/configuration'
15
16
 
16
17
  # Middleware
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erp_integration
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.54.0
4
+ version: 0.56.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Vermaas
@@ -329,6 +329,7 @@ files:
329
329
  - lib/erp_integration/purchase_order.rb
330
330
  - lib/erp_integration/purchase_order_line.rb
331
331
  - lib/erp_integration/purchase_request.rb
332
+ - lib/erp_integration/rate_limiter.rb
332
333
  - lib/erp_integration/resource.rb
333
334
  - lib/erp_integration/resources/errors.rb
334
335
  - lib/erp_integration/resources/persistence.rb
@@ -368,7 +369,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
368
369
  - !ruby/object:Gem::Version
369
370
  version: '0'
370
371
  requirements: []
371
- rubygems_version: 3.5.11
372
+ rubygems_version: 3.5.6
372
373
  signing_key:
373
374
  specification_version: 4
374
375
  summary: Connects Mejuri with third-party ERP vendors