easyship 0.2.0 → 0.4.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/.rubocop.yml +3 -0
- data/CHANGELOG.md +12 -1
- data/README.md +142 -3
- data/lib/easyship/client.rb +28 -9
- data/lib/easyship/configuration.rb +4 -1
- data/lib/easyship/pagination/cursor.rb +21 -2
- data/lib/easyship/rate_limiting/rate_limiter.rb +12 -0
- data/lib/easyship/rate_limiting/window_rate_limiter.rb +50 -0
- data/lib/easyship/version.rb +1 -1
- data/lib/easyship.rb +2 -0
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b584f647a6c1e9cd0379a94b3fc479c8f82691ab1ff8a36d797dae1c24e9c89
|
|
4
|
+
data.tar.gz: 5f288ce157f0b0b1c5e19f6c8a0a7851bf2bf1c5acb1c22b6660656ceed10204
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22a0cbabb4582b714d56827fdd6f29bee92958900b2204ba3fce15a62ed85139e327dcfaa39dcf3f935cdeab0440424fa00bf044f003282ac9212837c97a83a2
|
|
7
|
+
data.tar.gz: fd30c1f4d75e89f2cc01ea8b97040cd19717af6f5e7d6bdb26a661507de197882be0f10f9cfaae2f49ed92d65e8aa5c8cd9f6ee3b24e4ef747da158012a94abc
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [v0.4.0](https://github.com/mmarusyk/easyship/tree/v0.4.0) - 2026-01-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Support for custom headers by @mmarusyk in https://github.com/mmarusyk/easyship/pull/14
|
|
7
|
+
|
|
8
|
+
## [v0.3.0](https://github.com/mmarusyk/easyship/tree/v0.3.0) - 2025-11-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Rate limiting for cursor by @mmarusyk in https://github.com/mmarusyk/easyship/pull/13
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [v0.2.0](https://github.com/mmarusyk/easyship/tree/v0.2.0) - 2025-05-04
|
|
4
15
|
|
|
5
16
|
### Added
|
|
6
17
|
- Add response_body and response_header to Easyship::Error
|
data/README.md
CHANGED
|
@@ -58,10 +58,10 @@ Easyship.configure do |config|
|
|
|
58
58
|
end
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Configuration supports the next keys: `url`, `api_key`, `per_page`.
|
|
61
|
+
Configuration supports the next keys: `url`, `api_key`, `per_page`, `requests_per_second`, `requests_per_minute`, `headers`.
|
|
62
62
|
|
|
63
63
|
### Making Requests
|
|
64
|
-
`Easyship::Client` supports the next methods: `get`, `post`, `put`, `delete`.
|
|
64
|
+
`Easyship::Client` supports the next methods: `get`, `post`, `put`, `patch`, `delete`.
|
|
65
65
|
```ruby
|
|
66
66
|
Easyship::Client.instance.get('/2023-01/account')
|
|
67
67
|
```
|
|
@@ -131,7 +131,146 @@ end
|
|
|
131
131
|
shipments # Returns all shipments from all pages
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
To setup items
|
|
134
|
+
To setup items per page, use the key `per_page` in your configuration.
|
|
135
|
+
|
|
136
|
+
For Example:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# Global defaults
|
|
140
|
+
Easyship.configure do |config|
|
|
141
|
+
config.per_page = 100
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Per-call overrides (any of these are supported)
|
|
145
|
+
shipments = []
|
|
146
|
+
Easyship::Client.instance.get('/2023-01/shipments', {
|
|
147
|
+
per_page: 50,
|
|
148
|
+
}) do |page|
|
|
149
|
+
shipments.concat(page[:shipments])
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Rate limiting during pagination:
|
|
154
|
+
- The cursor has no default rate limiting; it uses nil to indicate that rate limiting is disabled.
|
|
155
|
+
- Use [Rate limiting documentation](https://developers.easyship.com/reference/rate-limit) for more details which values to set.
|
|
156
|
+
- You can override the limits per call, by passing `requests_per_second` and `requests_per_minute`.
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# Global defaults
|
|
162
|
+
Easyship.configure do |config|
|
|
163
|
+
config.requests_per_second = 10
|
|
164
|
+
config.requests_per_minute = 60
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Per-call overrides (any of these are supported)
|
|
168
|
+
shipments = []
|
|
169
|
+
Easyship::Client.instance.get('/2023-01/shipments', {
|
|
170
|
+
requests_per_second: 5,
|
|
171
|
+
requests_per_minute: 40,
|
|
172
|
+
}) do |page|
|
|
173
|
+
shipments.concat(page[:shipments])
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Custom Headers
|
|
178
|
+
|
|
179
|
+
You can pass custom headers both globally (via configuration) and per-request.
|
|
180
|
+
|
|
181
|
+
**Global Headers (via Configuration):**
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
Easyship.configure do |config|
|
|
185
|
+
config.url = 'api_url'
|
|
186
|
+
config.api_key = 'your_easyship_api_key'
|
|
187
|
+
config.headers = {
|
|
188
|
+
'X-Custom-Header' => 'custom-value'
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Per-Request Headers:**
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Override or add headers for a specific request
|
|
197
|
+
Easyship::Client.instance.get('/2023-01/account', {}, headers: {
|
|
198
|
+
'X-Request-ID' => 'unique-request-id'
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
# POST with custom headers
|
|
202
|
+
Easyship::Client.instance.post('/2023-01/shipment', payload, headers: {
|
|
203
|
+
'X-Idempotency-Key' => 'unique-key'
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
# PUT with custom headers
|
|
207
|
+
Easyship::Client.instance.put('/2023-01/shipment/123', payload, headers: {
|
|
208
|
+
'X-Custom-Header' => 'value'
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
# PATCH with custom headers
|
|
212
|
+
Easyship::Client.instance.patch('/2023-01/shipment/123', payload, headers: {
|
|
213
|
+
'X-Custom-Header' => 'value'
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
# DELETE with custom headers
|
|
217
|
+
Easyship::Client.instance.delete('/2023-01/shipment/123', {}, headers: {
|
|
218
|
+
'X-Custom-Header' => 'value'
|
|
219
|
+
})
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Note:** Per-request headers will override global headers if the same header key is used.
|
|
223
|
+
|
|
224
|
+
### Using Idempotency Keys
|
|
225
|
+
|
|
226
|
+
Idempotency keys are essential for safely retrying requests without accidentally performing the same operation twice. This is particularly important for POST requests that create resources (like shipments, orders, or payments).
|
|
227
|
+
|
|
228
|
+
**Example - Creating a Shipment with Idempotency:**
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
# Generate a unique key (e.g., UUID, database record ID, or any unique identifier)
|
|
232
|
+
idempotency_key = SecureRandom.uuid
|
|
233
|
+
|
|
234
|
+
payload = {
|
|
235
|
+
origin_country_alpha2: "SG",
|
|
236
|
+
destination_country_alpha2: "US",
|
|
237
|
+
tax_paid_by: "Recipient",
|
|
238
|
+
is_insured: false,
|
|
239
|
+
items: [
|
|
240
|
+
{
|
|
241
|
+
description: "Product",
|
|
242
|
+
actual_weight: 1.2,
|
|
243
|
+
height: 10,
|
|
244
|
+
width: 15,
|
|
245
|
+
length: 20,
|
|
246
|
+
declared_currency: "USD",
|
|
247
|
+
declared_customs_value: 50
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
begin
|
|
253
|
+
response = Easyship::Client.instance.post(
|
|
254
|
+
'/2023-01/shipments',
|
|
255
|
+
payload,
|
|
256
|
+
headers: { 'X-Idempotency-Key' => idempotency_key }
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# If this request fails due to network issues and you retry with the same key,
|
|
260
|
+
# the API will return the same shipment without creating a duplicate
|
|
261
|
+
rescue Easyship::Errors::EasyshipError => e
|
|
262
|
+
# Safe to retry with the same idempotency_key
|
|
263
|
+
retry
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Best Practices:**
|
|
268
|
+
|
|
269
|
+
- Use unique, random keys (UUIDs are recommended)
|
|
270
|
+
- Store the idempotency key with your order/shipment record in your database
|
|
271
|
+
- Reuse the same key when retrying a failed request
|
|
272
|
+
- Don't reuse keys across different operations or resources
|
|
273
|
+
- Keys typically expire after 24 hours (check API documentation)
|
|
135
274
|
|
|
136
275
|
## Development
|
|
137
276
|
|
data/lib/easyship/client.rb
CHANGED
|
@@ -16,46 +16,65 @@ module Easyship
|
|
|
16
16
|
@api_key = Easyship.configuration.api_key
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def get(path, params = {}, &block)
|
|
19
|
+
def get(path, params = {}, headers: {}, &block)
|
|
20
20
|
if block
|
|
21
21
|
Easyship::Pagination::Cursor.new(self, path, params).all(&block)
|
|
22
22
|
else
|
|
23
|
-
response = connection.get(path, params)
|
|
23
|
+
response = connection(headers).get(path, params)
|
|
24
24
|
|
|
25
25
|
handle_response(response)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def post(path, params = {})
|
|
30
|
-
response = connection.post(path, params.to_json)
|
|
29
|
+
def post(path, params = {}, headers: {})
|
|
30
|
+
response = connection(headers).post(path, params.to_json)
|
|
31
31
|
|
|
32
32
|
handle_response(response)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def put(path, params = {})
|
|
36
|
-
response = connection.put(path, params.to_json)
|
|
35
|
+
def put(path, params = {}, headers: {})
|
|
36
|
+
response = connection(headers).put(path, params.to_json)
|
|
37
37
|
|
|
38
38
|
handle_response(response)
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def delete(path, params = {})
|
|
42
|
-
response = connection.delete(path, params)
|
|
41
|
+
def delete(path, params = {}, headers: {})
|
|
42
|
+
response = connection(headers).delete(path, params)
|
|
43
|
+
|
|
44
|
+
handle_response(response)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def patch(path, params = {}, headers: {})
|
|
48
|
+
response = connection(headers).patch(path, params.to_json)
|
|
43
49
|
|
|
44
50
|
handle_response(response)
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
private
|
|
48
54
|
|
|
49
|
-
def connection
|
|
55
|
+
def connection(custom_headers = {})
|
|
50
56
|
Faraday.new(url: @url) do |faraday|
|
|
51
57
|
faraday.request :url_encoded
|
|
52
58
|
faraday.adapter Faraday.default_adapter
|
|
53
59
|
faraday.headers['Authorization'] = "Bearer #{@api_key}"
|
|
54
60
|
faraday.headers['Content-Type'] = 'application/json'
|
|
61
|
+
merge_headers(faraday, custom_headers)
|
|
55
62
|
faraday.use Easyship::Middleware::ErrorHandlerMiddleware
|
|
56
63
|
end
|
|
57
64
|
end
|
|
58
65
|
|
|
66
|
+
def merge_headers(faraday, custom_headers)
|
|
67
|
+
# Merge global configuration headers
|
|
68
|
+
Easyship.configuration.headers.each do |key, value|
|
|
69
|
+
faraday.headers[key] = value
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Merge request-specific headers (override globals)
|
|
73
|
+
custom_headers.each do |key, value|
|
|
74
|
+
faraday.headers[key] = value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
59
78
|
def handle_response(response)
|
|
60
79
|
Easyship::Handlers::ResponseBodyHandler.handle_response(response)
|
|
61
80
|
end
|
|
@@ -3,12 +3,15 @@
|
|
|
3
3
|
module Easyship
|
|
4
4
|
# Represents the configuration settings for the Easyship client.
|
|
5
5
|
class Configuration
|
|
6
|
-
attr_accessor :url, :api_key, :per_page
|
|
6
|
+
attr_accessor :url, :api_key, :per_page, :requests_per_second, :requests_per_minute, :headers
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
@url = nil
|
|
10
10
|
@api_key = nil
|
|
11
11
|
@per_page = 100 # Maximum possible number of items per page
|
|
12
|
+
@requests_per_second = nil
|
|
13
|
+
@requests_per_minute = nil
|
|
14
|
+
@headers = {}
|
|
12
15
|
end
|
|
13
16
|
end
|
|
14
17
|
end
|
|
@@ -4,20 +4,26 @@ module Easyship
|
|
|
4
4
|
module Pagination
|
|
5
5
|
# Represents a pagination object
|
|
6
6
|
class Cursor
|
|
7
|
-
|
|
7
|
+
CONFIGURATION_VARIABLES = %i[requests_per_second requests_per_minute].freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :client, :path, :params, :key, :per_page, :requests_per_second, :requests_per_minute
|
|
8
10
|
|
|
9
11
|
def initialize(client, path, params)
|
|
10
12
|
@client = client
|
|
11
13
|
@path = path
|
|
12
14
|
@params = params
|
|
13
15
|
@per_page = params[:per_page] || Easyship.configuration.per_page
|
|
16
|
+
@requests_per_second = params[:requests_per_second] || Easyship.configuration.requests_per_second
|
|
17
|
+
@requests_per_minute = params[:requests_per_minute] || Easyship.configuration.requests_per_minute
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def all
|
|
17
21
|
page = 1
|
|
18
22
|
|
|
19
23
|
loop do
|
|
20
|
-
|
|
24
|
+
limiter.throttle!
|
|
25
|
+
|
|
26
|
+
body = client.get(path, build_request_params(page: page))
|
|
21
27
|
|
|
22
28
|
break if body.nil? || body.empty?
|
|
23
29
|
|
|
@@ -28,6 +34,19 @@ module Easyship
|
|
|
28
34
|
page += 1
|
|
29
35
|
end
|
|
30
36
|
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_request_params(page:)
|
|
41
|
+
params.merge(page: page, per_page: per_page).except(*CONFIGURATION_VARIABLES)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def limiter
|
|
45
|
+
@limiter ||= Easyship::RateLimiting::WindowRateLimiter.new(
|
|
46
|
+
requests_per_second: requests_per_second,
|
|
47
|
+
requests_per_minute: requests_per_minute
|
|
48
|
+
)
|
|
49
|
+
end
|
|
31
50
|
end
|
|
32
51
|
end
|
|
33
52
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Easyship
|
|
4
|
+
module RateLimiting
|
|
5
|
+
# Represents WindowRateLimiter
|
|
6
|
+
class WindowRateLimiter < RateLimiter
|
|
7
|
+
attr_reader :timestamps, :requests_per_second, :requests_per_minute
|
|
8
|
+
|
|
9
|
+
def initialize(requests_per_second:, requests_per_minute:)
|
|
10
|
+
super()
|
|
11
|
+
@requests_per_second = requests_per_second
|
|
12
|
+
@requests_per_minute = requests_per_minute
|
|
13
|
+
@timestamps = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def throttle!
|
|
17
|
+
now = Time.now
|
|
18
|
+
|
|
19
|
+
timestamps << now
|
|
20
|
+
|
|
21
|
+
# Remove timestamps older than a minute
|
|
22
|
+
timestamps.reject! { |t| t < now - 60 }
|
|
23
|
+
|
|
24
|
+
check_second_window(now)
|
|
25
|
+
check_minute_window(now)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def check_second_window(now)
|
|
31
|
+
second_requests = timestamps.count { |t| t > now - 1 }
|
|
32
|
+
|
|
33
|
+
return if !requests_per_second || second_requests < requests_per_second
|
|
34
|
+
|
|
35
|
+
first_in_seconds = timestamps.find { |t| t > now - 1 }
|
|
36
|
+
sleep_time = (first_in_seconds + 1) - now
|
|
37
|
+
|
|
38
|
+
sleep(sleep_time) if sleep_time.positive?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def check_minute_window(now)
|
|
42
|
+
return if !requests_per_minute || timestamps.size < requests_per_minute
|
|
43
|
+
|
|
44
|
+
sleep_time = (timestamps.first + 60) - now
|
|
45
|
+
|
|
46
|
+
sleep(sleep_time) if sleep_time.positive?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/easyship/version.rb
CHANGED
data/lib/easyship.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative 'easyship/version'
|
|
4
4
|
require_relative 'easyship/configuration'
|
|
5
5
|
require_relative 'easyship/client'
|
|
6
|
+
require_relative 'easyship/rate_limiting/rate_limiter'
|
|
7
|
+
require_relative 'easyship/rate_limiting/window_rate_limiter'
|
|
6
8
|
|
|
7
9
|
# Provides configuration options for the Easyship gem.
|
|
8
10
|
module Easyship
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: easyship
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michael Marusyk
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-01-24 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: faraday
|
|
@@ -56,6 +57,8 @@ files:
|
|
|
56
57
|
- lib/easyship/handlers/response_body_handler.rb
|
|
57
58
|
- lib/easyship/middleware/error_handler_middleware.rb
|
|
58
59
|
- lib/easyship/pagination/cursor.rb
|
|
60
|
+
- lib/easyship/rate_limiting/rate_limiter.rb
|
|
61
|
+
- lib/easyship/rate_limiting/window_rate_limiter.rb
|
|
59
62
|
- lib/easyship/version.rb
|
|
60
63
|
- sig/easyship.rbs
|
|
61
64
|
homepage: https://rubygems.org/gems/easyship
|
|
@@ -67,6 +70,7 @@ metadata:
|
|
|
67
70
|
allowed_push_host: https://rubygems.org
|
|
68
71
|
source_code_uri: https://github.com/mmarusyk/easyship
|
|
69
72
|
changelog_uri: https://github.com/mmarusyk/easyship/blob/main/CHANGELOG.md
|
|
73
|
+
post_install_message:
|
|
70
74
|
rdoc_options: []
|
|
71
75
|
require_paths:
|
|
72
76
|
- lib
|
|
@@ -81,7 +85,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
81
85
|
- !ruby/object:Gem::Version
|
|
82
86
|
version: '0'
|
|
83
87
|
requirements: []
|
|
84
|
-
rubygems_version: 3.
|
|
88
|
+
rubygems_version: 3.5.22
|
|
89
|
+
signing_key:
|
|
85
90
|
specification_version: 4
|
|
86
91
|
summary: A Ruby client for integrating with Easyship's API for shipping and logistics
|
|
87
92
|
management.
|