fulfil-io 0.5.0 → 0.6.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
2
  SHA256:
3
- metadata.gz: 1100e113c9323c0fa742ece65d8a41923c060c382b1b97ad6c50ad20c550b5a9
4
- data.tar.gz: 80535f7bea24218d958812bbd8e4ab789ce966c346e5dfa891369160fc04e536
3
+ metadata.gz: 0c58e4104ae9106550bca7c000cbf3f576f551c9e4512cee93dc74dd0aa5604f
4
+ data.tar.gz: ee0644f88360b5356efa034833772a54d207ce4be7e81040219e8d66019ff301
5
5
  SHA512:
6
- metadata.gz: b1ea6e403ab80bb108a90dff5f7ad58c39c228ed610ce497c631b5ae45a3ef5d2912100acd95ee33d7f2aa7c5609b71300c7a2322e1b2efcec688646573bf8da
7
- data.tar.gz: d63d23350f0775bac73f28fdbabd0645798ed28a0466e7851ade38328a9f17e7a75982bc6bada6c1d951e4eb3ed0a4ffd872c79a6dc3c848f4308ddcfd11db16
6
+ metadata.gz: d697a2153dcbcd379621b59bc7ef8af428c4f6ecc2445d454afc38eddb34247430279136f758fe52e538abcda923c386dbc4d7f2af00c27e9e41c26f71a4c5e2
7
+ data.tar.gz: e6e891f839dd472f24f1dc89ce1189cb80b9218ac09c35c4141c0dd0b575097ecb0d238d47de8e688bc69c3298da0f4e1b2478ebfed1010d139c3fb4087f0ccf
data/README.md CHANGED
@@ -1,36 +1,40 @@
1
- # Fulfil.io Rubygem
1
+ [![Tests](https://github.com/knowndecimal/fulfil/actions/workflows/tests.yml/badge.svg)](https://github.com/knowndecimal/fulfil/actions/workflows/tests.yml)
2
2
 
3
- [![CircleCI](https://circleci.com/gh/knowndecimal/fulfil.svg?style=svg&circle-token=da80ea6500af15b3a795a3913efe35742bab94c1)](https://circleci.com/gh/knowndecimal/fulfil)
3
+ # Fulfil.io Rubygem
4
4
 
5
- [Fulfil.io](https://fulfil.io)
5
+ A Ruby library for the [Fulfil.io](https://fulfil.io) API.
6
6
 
7
7
  ## Installation
8
8
 
9
9
  Add this line to your application's Gemfile:
10
10
 
11
11
  ```ruby
12
- gem 'fulfil-io', require: 'fulfil'
12
+ gem 'fulfil-io', require: 'fulfil'
13
13
  ```
14
14
 
15
15
  And then execute:
16
16
 
17
- $ bundle install
17
+ ```shell
18
+ $ bundle install
19
+ ```
18
20
 
19
21
  Or install it yourself as:
20
22
 
21
- $ gem install fulfil-io
23
+ ```shell
24
+ $ gem install fulfil-io
25
+ ```
22
26
 
23
27
  ## Usage
24
28
 
25
29
  Environment variables:
26
30
 
27
- - FULFIL_SUBDOMAIN - required to be set.
28
- - FULFIL_TOKEN - required for oauth bearer authentication
29
- - FULFIL_API_KEY - required for authentication via the X-API-KEY request header
31
+ - **FULFIL_SUBDOMAIN:** - always required to use the gem.
32
+ - **FULFIL_OAUTH_TOKEN:** required for oauth bearer authentication
33
+ - **FULFIL_API_KEY:** required for authentication via the `X-API-KEY` request header
30
34
 
31
- **Note:** When FULFIL_TOKEN is present, the FULFIL_API_KEY will be ignored. So,
35
+ > **Note:** When `FULFIL_OAUTH_TOKEN` is present, the `FULFIL_API_KEY` will be ignored. So,
32
36
  if oauth doesn't work, returning an Unauthorized error, to use the
33
- FULFIL_API_KEY, the FULFIL_TOKEN shouldn't be specified.
37
+ `FULFIL_API_KEY`, the `FULFIL_OAUTH_TOKEN` shouldn't be specified.
34
38
 
35
39
  ```ruby
36
40
  require 'fulfil' # this is necessary only in case of running without bundler
@@ -123,6 +127,26 @@ report = Fulfil::Report.new(client: fulfil, report_name: 'account.tax.summary.ir
123
127
  report.execute(start_date: Date.new(2020, 12, 1), end_date: Date.new(2020, 12, 31))
124
128
  ```
125
129
 
130
+ ## Rate limits
131
+
132
+ Fulfil's API applies rate limits to the API requests that it receives. Every request is subject to throttling under the general limits. In addition, there are resource-based rate limits and throttles.
133
+
134
+ This gem exposes an API for checking your current rate limits (note: the gem only knows about the rate limit after a request to Fulfil's API has been made).
135
+
136
+ Whenever you reached the rate limit, the `Fulfil::RateLimitExceeded` exception is being raised. You can use the information on the `Fulfil.rate_limit` to find out what to do next.
137
+
138
+ ```ruby
139
+ $ Fulfil.rate_limit.requests_left?
140
+ => true
141
+
142
+ # The maximum number of requests you're permitted to make per second.
143
+ $ Fulfil.rate_limit.limit
144
+ => 9
145
+
146
+ # The time at which the current rate limit window resets in UTC epoch seconds.
147
+ $ Fulfil.rate_limit.resets_at
148
+ => #<DateTime: 2022-01-21T16:36:01-04:00 />
149
+ ```
126
150
  ## Development
127
151
 
128
152
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -145,7 +169,7 @@ For non-client tests, create the test class or case.
145
169
 
146
170
  For client tests, you'll need to add a couple steps. If running against a real
147
171
  backend, you'll need to provide a couple of environment variables:
148
- `FULFIL_SUBDOMAIN` and `FULFIL_TOKEN`. Additionally, pass `debug: true` to the
172
+ `FULFIL_SUBDOMAIN` and `FULFIL_OAUTH_TOKEN`. Additionally, pass `debug: true` to the
149
173
  client instance in the test. This will output the response body. Webmock will
150
174
  probably complain that real requests aren't allowed at this point, offering you
151
175
  the stub. We don't need most of that.
data/lib/fulfil/client.rb CHANGED
@@ -7,7 +7,6 @@ require 'fulfil/response_parser'
7
7
  module Fulfil
8
8
  SUBDOMAIN = ENV['FULFIL_SUBDOMAIN']
9
9
  API_KEY = ENV['FULFIL_API_KEY']
10
- OAUTH_TOKEN = ENV['FULFIL_TOKEN']
11
10
 
12
11
  class Client
13
12
  class InvalidClientError < StandardError
@@ -24,7 +23,7 @@ module Fulfil
24
23
 
25
24
  class ResponseError < StandardError; end
26
25
 
27
- def initialize(subdomain: SUBDOMAIN, token: OAUTH_TOKEN, headers: { 'X-API-KEY' => API_KEY }, debug: false)
26
+ def initialize(subdomain: SUBDOMAIN, token: oauth_token, headers: { 'X-API-KEY' => API_KEY }, debug: false)
28
27
  @subdomain = subdomain
29
28
  @token = token
30
29
  @debug = debug
@@ -112,6 +111,15 @@ module Fulfil
112
111
 
113
112
  private
114
113
 
114
+ def oauth_token
115
+ if ENV['FULFIL_TOKEN']
116
+ puts "You're using an deprecated environment variable. Please update your " \
117
+ 'FULFIL_TOKEN to FULFIL_OAUTH_TOKEN.'
118
+ end
119
+
120
+ ENV['FULFIL_OAUTH_TOKEN'] || ENV['FULFIL_TOKEN']
121
+ end
122
+
115
123
  def parse(result: nil, results: [])
116
124
  if result
117
125
  parse_single(result: result)
@@ -159,12 +167,10 @@ module Fulfil
159
167
  end
160
168
 
161
169
  def client
162
- return @client if defined?(@client)
163
-
164
- @client = HTTP.persistent(base_url).use(logging: @debug ? { logger: Logger.new(STDOUT) } : {})
165
- @client = @client.auth("Bearer #{@token}") if @token
166
- @client = @client.headers(@headers)
167
- @client
170
+ client = HTTP.use(logging: @debug ? { logger: Logger.new(STDOUT) } : {})
171
+ client = client.auth("Bearer #{@token}") if @token
172
+ client = client.headers(@headers)
173
+ client
168
174
  end
169
175
  end
170
176
  end
data/lib/fulfil/error.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  module Fulfil
4
4
  class Error < StandardError; end
5
5
 
6
+ class RateLimitExceeded < Error; end
7
+
6
8
  # The `Fulfil::HttpError` is raised whenever an API request returns a 400+ HTTP status code.
7
9
  # See `Fulfil::ResponseHandler` for more information.
8
10
  class HttpError < Error
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fulfil
4
+ # The `Fulfil::RateLimit` allows clients to keep track of their API usage and
5
+ # to analyze Fulfil's response HTTP headers.
6
+ class RateLimit
7
+ attr_accessor :limit, :requests_left, :resets_at
8
+
9
+ # Analyses the rate limit based on the response headers from Fulfil.
10
+ # @param headers [HTTP::Headers] The HTTP response headers from Fulfil.
11
+ # @return [Fulfil::RateLimit]
12
+ def analyse!(headers)
13
+ rate_limit_headers = RateLimitHeaders.new(headers)
14
+
15
+ self.limit = rate_limit_headers.limit
16
+ self.requests_left = rate_limit_headers.requests_left
17
+ self.resets_at = rate_limit_headers.resets_at
18
+
19
+ raise Fulfil::RateLimitExceeded unless requests_left?
20
+ end
21
+
22
+ # Returns whether there are any requests left in the current rate limit window.
23
+ # @return [Boolean]
24
+ def requests_left?
25
+ requests_left&.positive?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fulfil
4
+ # The `Fulfil::RateLimitHeaders` parses Fulfil HTTP rate limit headers and
5
+ # formats them to a more usable format.
6
+ class RateLimitHeaders
7
+ # Test suites might mock (or at least should mock) the requests to Fulfil.
8
+ # However, most of these test suites will not mock the response headers.
9
+ # To make sure those test suites don't break, we're setting some defaults for them.
10
+ DEFAULT_REQUEST_LIMIT = 10
11
+ DEFAULT_REQUESTS_LEFT = 9
12
+ DEFAULT_RESETS_AT = nil
13
+
14
+ attr_reader :limit, :requests_left, :resets_at
15
+
16
+ def initialize(headers = {})
17
+ self.limit = headers['X-RateLimit-Limit'] || DEFAULT_REQUEST_LIMIT
18
+ self.requests_left = headers['X-RateLimit-Remaining'] || DEFAULT_REQUESTS_LEFT
19
+ self.resets_at = headers['X-RateLimit-Reset'] || DEFAULT_RESETS_AT
20
+ end
21
+
22
+ # Sets the maximum number of requests you're permitted to make per second.
23
+ # @param value [String] The maximum number of requests per second.
24
+ # @return [Integer] The maximum number of requests per second.
25
+ def limit=(value)
26
+ @limit = value.to_i
27
+ end
28
+
29
+ # Sets number of requests remaining in the current rate limit window.
30
+ # @param value [String] The remaining number of requests for the current time window.
31
+ # @return [Integer] The remaining number of requests for the current time window.
32
+ def requests_left=(value)
33
+ @requests_left = value.to_i
34
+ end
35
+
36
+ # Sets the time at which the current rate limit window resets in UTC epoch seconds.
37
+ # @param value [Integer|nil] Time as an integer in UTC epoch seconds.
38
+ # @return [DataTime|nil] The moment the rate limit resets.
39
+ def resets_at=(value)
40
+ @resets_at =
41
+ if value.nil?
42
+ nil
43
+ else
44
+ Time.at(value.to_i).utc.to_datetime
45
+ end
46
+ end
47
+ end
48
+ end
@@ -31,6 +31,17 @@ module Fulfil
31
31
  end
32
32
 
33
33
  def verify!
34
+ verify_rate_limits!
35
+ verify_http_status_code!
36
+ end
37
+
38
+ private
39
+
40
+ def verify_rate_limits!
41
+ Fulfil.rate_limit.analyse!(@response.headers)
42
+ end
43
+
44
+ def verify_http_status_code!
34
45
  return true unless @status_code >= 400
35
46
 
36
47
  raise HTTP_ERROR_CODES.fetch(@status_code, Fulfil::HttpError).new(
@@ -43,8 +54,6 @@ module Fulfil
43
54
  )
44
55
  end
45
56
 
46
- private
47
-
48
57
  def response_body
49
58
  @response_body ||= @response.parse
50
59
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fulfil
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/fulfil.rb CHANGED
@@ -8,5 +8,12 @@ require 'fulfil/interactive_report'
8
8
  require 'fulfil/response_handler'
9
9
  require 'fulfil/response_parser'
10
10
 
11
+ # Rate limiting
12
+ require 'fulfil/rate_limit'
13
+ require 'fulfil/rate_limit_headers'
14
+
11
15
  module Fulfil
16
+ def self.rate_limit
17
+ @rate_limit ||= RateLimit.new
18
+ end
12
19
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fulfil-io
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Moore
@@ -9,22 +9,28 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-12-26 00:00:00.000000000 Z
12
+ date: 2022-03-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: http
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - ">="
19
19
  - !ruby/object:Gem::Version
20
20
  version: 4.4.1
21
+ - - "<"
22
+ - !ruby/object:Gem::Version
23
+ version: 5.1.0
21
24
  type: :runtime
22
25
  prerelease: false
23
26
  version_requirements: !ruby/object:Gem::Requirement
24
27
  requirements:
25
- - - "~>"
28
+ - - ">="
26
29
  - !ruby/object:Gem::Version
27
30
  version: 4.4.1
31
+ - - "<"
32
+ - !ruby/object:Gem::Version
33
+ version: 5.1.0
28
34
  - !ruby/object:Gem::Dependency
29
35
  name: bundler
30
36
  requirement: !ruby/object:Gem::Requirement
@@ -109,6 +115,26 @@ dependencies:
109
115
  - - ">="
110
116
  - !ruby/object:Gem::Version
111
117
  version: '0'
118
+ - !ruby/object:Gem::Dependency
119
+ name: dotenv
120
+ requirement: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2.7'
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 2.7.6
128
+ type: :development
129
+ prerelease: false
130
+ version_requirements: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: '2.7'
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 2.7.6
112
138
  description:
113
139
  email:
114
140
  - chris@knowndecimal.com
@@ -129,6 +155,8 @@ files:
129
155
  - lib/fulfil/interactive_report.rb
130
156
  - lib/fulfil/model.rb
131
157
  - lib/fulfil/query.rb
158
+ - lib/fulfil/rate_limit.rb
159
+ - lib/fulfil/rate_limit_headers.rb
132
160
  - lib/fulfil/response_handler.rb
133
161
  - lib/fulfil/response_parser.rb
134
162
  - lib/fulfil/version.rb
@@ -144,14 +172,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
172
  requirements:
145
173
  - - ">="
146
174
  - !ruby/object:Gem::Version
147
- version: 2.3.0
175
+ version: '2.4'
148
176
  required_rubygems_version: !ruby/object:Gem::Requirement
149
177
  requirements:
150
178
  - - ">="
151
179
  - !ruby/object:Gem::Version
152
180
  version: '0'
153
181
  requirements: []
154
- rubygems_version: 3.1.6
182
+ rubygems_version: 3.3.7
155
183
  signing_key:
156
184
  specification_version: 4
157
185
  summary: Interact with the Fulfil.io API