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 +4 -4
- data/README.md +36 -12
- data/lib/fulfil/client.rb +14 -8
- data/lib/fulfil/error.rb +2 -0
- data/lib/fulfil/rate_limit.rb +28 -0
- data/lib/fulfil/rate_limit_headers.rb +48 -0
- data/lib/fulfil/response_handler.rb +11 -2
- data/lib/fulfil/version.rb +1 -1
- data/lib/fulfil.rb +7 -0
- metadata +34 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c58e4104ae9106550bca7c000cbf3f576f551c9e4512cee93dc74dd0aa5604f
|
4
|
+
data.tar.gz: ee0644f88360b5356efa034833772a54d207ce4be7e81040219e8d66019ff301
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d697a2153dcbcd379621b59bc7ef8af428c4f6ecc2445d454afc38eddb34247430279136f758fe52e538abcda923c386dbc4d7f2af00c27e9e41c26f71a4c5e2
|
7
|
+
data.tar.gz: e6e891f839dd472f24f1dc89ce1189cb80b9218ac09c35c4141c0dd0b575097ecb0d238d47de8e688bc69c3298da0f4e1b2478ebfed1010d139c3fb4087f0ccf
|
data/README.md
CHANGED
@@ -1,36 +1,40 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
17
|
+
```shell
|
18
|
+
$ bundle install
|
19
|
+
```
|
18
20
|
|
19
21
|
Or install it yourself as:
|
20
22
|
|
21
|
-
|
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
|
28
|
-
-
|
29
|
-
- FULFIL_API_KEY
|
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
|
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
|
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 `
|
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:
|
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
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|
data/lib/fulfil/version.rb
CHANGED
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.
|
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:
|
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.
|
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.
|
182
|
+
rubygems_version: 3.3.7
|
155
183
|
signing_key:
|
156
184
|
specification_version: 4
|
157
185
|
summary: Interact with the Fulfil.io API
|