duffel_api 0.2.0 → 0.3.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: 3f34f8f28b32ebbcce50624f302a8c571322804417841653645dd6ea659f8a22
4
- data.tar.gz: fab5945c12ed6054f01330dc2bd62cb814cd90f486b7dc36ed4caa0c8197617c
3
+ metadata.gz: c46f165ae24365df8a17eb35d99ddb7ffed2a5dc170d84e8258d17c1f120c62f
4
+ data.tar.gz: 9e566d5f0b79b9dd13d5ac873f095a575ea4a23c11dbcae00d91f13940816330
5
5
  SHA512:
6
- metadata.gz: 89708a5b441623dfecaee21f06bfba8da70e93b251d2587895a1b7ea3a9df1375251ae5a85de0a4550bf96ef46249a3c8d341f85c187d268cdf4fc6c6029c2da
7
- data.tar.gz: c0e3165cf5a164dab4d2da40e9171fc2c680800be4a2f43c96e18556c75c9890555bec6cd3455c90de3c8141347bd32fc13d50eb78e33336ce8954a38387633e
6
+ metadata.gz: e03f1705659ccd04c14cc5d479f8ca13f9ec9a67338eb64aa824a9ac72ee2e21133e0725bc04d7d5772343ce3dc7d4e146184400836535ef49b0edd077b39514
7
+ data.tar.gz: f99d04e62d5d518affba0baa592c5f10a118bc1b7cac8bcb1ada16c778a2dceb7e4cc0c16be80bc80be9bd56cf3e7df6e46bc80736019a299db04824ca62ec4c
data/.rubocop.yml CHANGED
@@ -11,3 +11,6 @@ RSpec/ExampleLength:
11
11
 
12
12
  RSpec/MultipleExpectations:
13
13
  Enabled: false
14
+
15
+ Rails:
16
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [0.3.0] - 2022-02-15
2
+
3
+ - Automatically retry rate-limited requests (thanks to @ferrisoxide for the contribution!)
4
+ - Paginate through records with `#all` 200 records at a time by default, rather than 50 records at a time
5
+
1
6
  ## [0.2.0] - 2022-01-11
2
7
 
3
8
  - Add `WebhookEvent.genuine?` for checking whether a webhook event was genuinely sent by Duffel
data/Gemfile CHANGED
@@ -6,13 +6,13 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  group :development, :test do
9
- gem "gc_ruboconfig", "~> 2.30.0"
9
+ gem "gc_ruboconfig", "~> 3.0.0"
10
10
  gem "pry", "~> 0.14.1"
11
11
  gem "rake", "~> 13.0"
12
- gem "rspec", "~> 3.10.0"
12
+ gem "rspec", "~> 3.11.0"
13
13
  gem "rspec-its", "~> 1.3.0"
14
14
  gem "rspec_junit_formatter", "~> 0.5.0"
15
- gem "rubocop", "~> 1.24.0"
15
+ gem "rubocop", "~> 1.25.0"
16
16
  gem "rubocop-rake", "~> 0.6.0"
17
17
  gem "simplecov", "~> 0.21.2"
18
18
  gem "webmock", "~> 3.14.0"
data/README.md CHANGED
@@ -20,7 +20,7 @@ A Ruby client library for the [Duffel API](https://duffel.com/docs/api).
20
20
  In most cases, you'll want to add `duffel_api` to your project as a dependency by listing it in your `Gemfile`, and then running `bundle`:
21
21
 
22
22
  ```ruby
23
- gem "duffel_api", "~> 0.2.0"
23
+ gem "duffel_api", "~> 0.3.0"
24
24
  ```
25
25
 
26
26
  You can install `duffel_api` outside of the context of a project by running `gem install duffel_api` - for example if you want to play with the client library in `irb`.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "duffel_api"
4
+ require "bigdecimal"
4
5
 
5
6
  client = DuffelAPI::Client.new(
6
7
  access_token: ENV["DUFFEL_ACCESS_TOKEN"],
@@ -50,8 +51,9 @@ puts "Adding #{available_baggage['metadata']['maximum_weight_kg']}kg extra bagga
50
51
  "#{available_baggage['total_amount']} " \
51
52
  "#{available_baggage['total_currency']}"
52
53
 
53
- total_amount = priced_offer.total_amount.to_f +
54
- available_baggage["total_amount"].to_f
54
+ total_amount = (
55
+ BigDecimal(priced_offer.total_amount) + BigDecimal(available_baggage["total_amount"])
56
+ ).to_s("F")
55
57
 
56
58
  order = client.orders.create(params: {
57
59
  selected_offers: [priced_offer.id],
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "duffel_api"
4
+ require "bigdecimal"
4
5
 
5
6
  client = DuffelAPI::Client.new(
6
7
  access_token: ENV["DUFFEL_ACCESS_TOKEN"],
@@ -57,8 +58,10 @@ puts "Adding seat #{available_seat['designator']} costing " \
57
58
  "#{available_seat_service['total_amount']} " \
58
59
  "#{available_seat_service['total_currency']}"
59
60
 
60
- total_amount = priced_offer.total_amount.to_f +
61
- available_seat_service["total_amount"].to_f
61
+ total_amount = (
62
+ BigDecimal(priced_offer.total_amount) +
63
+ BigDecimal(available_seat_service["total_amount"])
64
+ ).to_s("F")
62
65
 
63
66
  order = client.orders.create(params: {
64
67
  selected_offers: [priced_offer.id],
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "duffel_api"
4
+ require "bigdecimal"
4
5
 
5
6
  client = DuffelAPI::Client.new(
6
7
  access_token: ENV["DUFFEL_ACCESS_TOKEN"],
@@ -47,7 +48,9 @@ available_service = priced_offer.available_services.first
47
48
  puts "Adding an extra bag with service #{available_service['id']}, " \
48
49
  "costing #{available_service['total_amount']} #{available_service['total_currency']}"
49
50
 
50
- total_amount = priced_offer.total_amount.to_f + available_service["total_amount"].to_f
51
+ total_amount = (
52
+ BigDecimal(priced_offer.total_amount) + BigDecimal(available_service["total_amount"])
53
+ ).to_s("F")
51
54
 
52
55
  order = client.orders.create(params: {
53
56
  selected_offers: [priced_offer.id],
@@ -19,6 +19,7 @@ module DuffelAPI
19
19
  root_url, @path_prefix = unpack_url(base_url)
20
20
 
21
21
  @connection = Faraday.new(root_url) do |faraday|
22
+ faraday.request :rate_limiter
22
23
  faraday.response :raise_duffel_errors
23
24
 
24
25
  faraday.adapter(:net_http)
@@ -8,7 +8,6 @@ module DuffelAPI
8
8
  UNEXPECTED_ERROR_STATUSES = (501..599).freeze
9
9
  EXPECTED_ERROR_STATUSES = (400..500).freeze
10
10
 
11
- # rubocop:disable Metrics/AbcSize
12
11
  # Handles a completed (Faraday) request and raises an error, if appropriate
13
12
  #
14
13
  # @param [Faraday::Env] env
@@ -30,7 +29,6 @@ module DuffelAPI
30
29
  raise error_class.new(error, response)
31
30
  end
32
31
  end
33
- # rubocop:enable Metrics/AbcSize
34
32
 
35
33
  private
36
34
 
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "logger"
5
+
6
+ module DuffelAPI
7
+ module Middlewares
8
+ class RateLimiter < Faraday::Middleware
9
+ class << self
10
+ attr_accessor :ratelimit_limit, :ratelimit_remaining, :ratelimit_reset
11
+
12
+ def mutex
13
+ @mutex ||= Mutex.new
14
+ end
15
+ end
16
+
17
+ def initialize(app, options = {})
18
+ super(app, options)
19
+ end
20
+
21
+ def call(env)
22
+ sleep_until_ratelimit_reset if rate_limited?
23
+
24
+ app.call(env).tap do |response|
25
+ headers = response.env.response_headers
26
+
27
+ RateLimiter.mutex.synchronize do
28
+ RateLimiter.ratelimit_limit = new_ratelimit_limit(headers)
29
+ RateLimiter.ratelimit_remaining = new_ratelimit_remaining(headers)
30
+ RateLimiter.ratelimit_reset = new_ratelimit_reset(headers)
31
+ end
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # NOTE: The check for 'ratelimit-limit' vs 'Ratelimit-Limit' is required
38
+ # because RSpec and Rails proper handle HTTP headers differently.
39
+ # WebMock converts the header to the latter style, whereas when running
40
+ # through Rails it will be converted to the former.
41
+ def header_value_with_indifferent_key(headers, key)
42
+ headers.find { |k, _v| k.casecmp(key).zero? }&.at(1)
43
+ end
44
+
45
+ def new_ratelimit_limit(headers)
46
+ header_value_with_indifferent_key(headers, "ratelimit-limit")&.to_i
47
+ end
48
+
49
+ def new_ratelimit_remaining(headers)
50
+ header_value_with_indifferent_key(headers, "ratelimit-remaining")&.to_i
51
+ end
52
+
53
+ def new_ratelimit_reset(headers)
54
+ reset_time = header_value_with_indifferent_key(headers, "ratelimit-reset")
55
+ reset_time.nil? ? nil : DateTime.parse(reset_time).to_time
56
+ end
57
+
58
+ def rate_limited?
59
+ return false if RateLimiter.ratelimit_reset.nil?
60
+ return false if RateLimiter.ratelimit_remaining.nil?
61
+
62
+ RateLimiter.ratelimit_remaining.zero?
63
+ end
64
+
65
+ def sleep_until_ratelimit_reset
66
+ sleep_time = (RateLimiter.ratelimit_reset.to_i - Time.now.to_i) + 1
67
+ return unless sleep_time.positive?
68
+
69
+ ::Logger.new($stdout).info(
70
+ "Duffel rate-limit hit. Sleeping for #{sleep_time} seconds",
71
+ )
72
+ Kernel.sleep(sleep_time)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ Faraday::Request.register_middleware rate_limiter: DuffelAPI::Middlewares::RateLimiter
@@ -25,7 +25,6 @@ module DuffelAPI
25
25
  attr_reader :total_emissions_kg
26
26
  attr_reader :updated_at
27
27
 
28
- # rubocop:disable Metrics/AbcSize
29
28
  def initialize(object, response = nil)
30
29
  @object = object
31
30
 
@@ -54,7 +53,6 @@ module DuffelAPI
54
53
 
55
54
  super(object, response)
56
55
  end
57
- # rubocop:enable Metrics/AbcSize
58
56
  end
59
57
  end
60
58
  end
@@ -25,7 +25,6 @@ module DuffelAPI
25
25
  attr_reader :total_amount
26
26
  attr_reader :total_currency
27
27
 
28
- # rubocop:disable Metrics/AbcSize
29
28
  def initialize(object, response = nil)
30
29
  @object = object
31
30
 
@@ -52,7 +51,6 @@ module DuffelAPI
52
51
 
53
52
  super(object, response)
54
53
  end
55
- # rubocop:enable Metrics/AbcSize
56
54
  end
57
55
  end
58
56
  end
@@ -19,7 +19,6 @@ module DuffelAPI
19
19
  attr_reader :refund_to
20
20
  attr_reader :slices
21
21
 
22
- # rubocop:disable Metrics/AbcSize
23
22
  def initialize(object, response = nil)
24
23
  @object = object
25
24
 
@@ -40,7 +39,6 @@ module DuffelAPI
40
39
 
41
40
  super(object, response)
42
41
  end
43
- # rubocop:enable Metrics/AbcSize
44
42
  end
45
43
  end
46
44
  end
@@ -19,7 +19,6 @@ module DuffelAPI
19
19
  attr_reader :slices
20
20
  attr_reader :updated_at
21
21
 
22
- # rubocop:disable Metrics/AbcSize
23
22
  def initialize(object, response = nil)
24
23
  @object = object
25
24
 
@@ -40,7 +39,6 @@ module DuffelAPI
40
39
 
41
40
  super(object, response)
42
41
  end
43
- # rubocop:enable Metrics/AbcSize
44
42
  end
45
43
  end
46
44
  end
@@ -22,7 +22,6 @@ module DuffelAPI
22
22
  attr_reader :status
23
23
  attr_reader :updated_at
24
24
 
25
- # rubocop:disable Metrics/AbcSize
26
25
  def initialize(object, response = nil)
27
26
  @object = object
28
27
 
@@ -46,7 +45,6 @@ module DuffelAPI
46
45
 
47
46
  super(object, response)
48
47
  end
49
- # rubocop:enable Metrics/AbcSize
50
48
  end
51
49
  end
52
50
  end
@@ -17,7 +17,6 @@ module DuffelAPI
17
17
  attr_reader :status
18
18
  attr_reader :updated_at
19
19
 
20
- # rubocop:disable Metrics/AbcSize
21
20
  def initialize(object, response = nil)
22
21
  @object = object
23
22
 
@@ -36,7 +35,6 @@ module DuffelAPI
36
35
 
37
36
  super(object, response)
38
37
  end
39
- # rubocop:enable Metrics/AbcSize
40
38
  end
41
39
  end
42
40
  end
@@ -22,13 +22,18 @@ module DuffelAPI
22
22
  end
23
23
 
24
24
  # Returns an `Enumerator` which can automatically cycle through multiple
25
- # pages of `Resources;:Aircraft`
25
+ # pages of `Resources::Aircraft`.
26
+ #
27
+ # By default, this will use pages of 200 results under the hood, but this
28
+ # can be customised by specifying the `:limit` option in the `:params`.
26
29
  #
27
30
  # @param options [Hash] options passed to `#list`, for example `:params` to
28
31
  # send an HTTP querystring with filters
29
32
  # @return [Enumerator]
30
33
  # @raise [Errors::Error] when the Duffel API returns an error
31
34
  def all(options = {})
35
+ options[:params] = DEFAULT_ALL_PARAMS.merge(options[:params] || {})
36
+
32
37
  DuffelAPI::Paginator.new(
33
38
  service: self,
34
39
  options: options,
@@ -21,13 +21,18 @@ module DuffelAPI
21
21
  end
22
22
 
23
23
  # Returns an `Enumerator` which can automatically cycle through multiple
24
- # pages of `Resources;:Airline`s
24
+ # pages of `Resources::Airline`s.
25
+ #
26
+ # By default, this will use pages of 200 results under the hood, but this
27
+ # can be customised by specifying the `:limit` option in the `:params`.
25
28
  #
26
29
  # @param options [Hash] options passed to `#list`, for example `:params` to
27
30
  # send an HTTP querystring with filters
28
31
  # @return [Enumerator]
29
32
  # @raise [Errors::Error] when the Duffel API returns an error
30
33
  def all(options = {})
34
+ options[:params] = DEFAULT_ALL_PARAMS.merge(options[:params] || {})
35
+
31
36
  DuffelAPI::Paginator.new(
32
37
  service: self,
33
38
  options: options,
@@ -22,13 +22,18 @@ module DuffelAPI
22
22
  end
23
23
 
24
24
  # Returns an `Enumerator` which can automatically cycle through multiple
25
- # pages of `Resources;:Airport`s
25
+ # pages of `Resources::Airport`s.
26
+ #
27
+ # By default, this will use pages of 200 results under the hood, but this
28
+ # can be customised by specifying the `:limit` option in the `:params`.
26
29
  #
27
30
  # @param options [Hash] options passed to `#list`, for example `:params` to
28
31
  # send an HTTP querystring with filters
29
32
  # @return [Enumerator]
30
33
  # @raise [Errors::Error] when the Duffel API returns an error
31
34
  def all(options = {})
35
+ options[:params] = DEFAULT_ALL_PARAMS.merge(options[:params] || {})
36
+
32
37
  DuffelAPI::Paginator.new(
33
38
  service: self,
34
39
  options: options,
@@ -7,6 +7,9 @@ module DuffelAPI
7
7
  class BaseService
8
8
  extend Forwardable
9
9
 
10
+ # Default params to use with the auto-paginating `#all` method
11
+ DEFAULT_ALL_PARAMS = { limit: 200 }.freeze
12
+
10
13
  # Sets up a resource-specific service based on an API service
11
14
  #
12
15
  # @param api_service [APIService]
@@ -59,13 +59,18 @@ module DuffelAPI
59
59
  end
60
60
 
61
61
  # Returns an `Enumerator` which can automatically cycle through multiple
62
- # pages of `Resources;:OfferRequest`s
62
+ # pages of `Resources::OfferRequest`s.
63
+ #
64
+ # By default, this will use pages of 200 results under the hood, but this
65
+ # can be customised by specifying the `:limit` option in the `:params`.
63
66
  #
64
67
  # @param options [Hash] options passed to `#list`, for example `:params` to
65
68
  # send an HTTP querystring with filters
66
69
  # @return [Enumerator]
67
70
  # @raise [Errors::Error] when the Duffel API returns an error
68
71
  def all(options = {})
72
+ options[:params] = DEFAULT_ALL_PARAMS.merge(options[:params] || {})
73
+
69
74
  Paginator.new(
70
75
  service: self,
71
76
  options: options,
@@ -22,13 +22,18 @@ module DuffelAPI
22
22
  end
23
23
 
24
24
  # Returns an `Enumerator` which can automatically cycle through multiple
25
- # pages of `Resources;:Offers`s
25
+ # pages of `Resources::Offers`s.
26
+ #
27
+ # By default, this will use pages of 200 results under the hood, but this
28
+ # can be customised by specifying the `:limit` option in the `:params`.
26
29
  #
27
30
  # @param options [Hash] options passed to `#list`, for example `:params` to
28
31
  # send an HTTP querystring with filters
29
32
  # @return [Enumerator]
30
33
  # @raise [Errors::Error] when the Duffel API returns an error
31
34
  def all(options = {})
35
+ options[:params] = DEFAULT_ALL_PARAMS.merge(options[:params] || {})
36
+
32
37
  Paginator.new(
33
38
  service: self,
34
39
  options: options,
@@ -71,13 +71,18 @@ module DuffelAPI
71
71
  end
72
72
 
73
73
  # Returns an `Enumerator` which can automatically cycle through multiple
74
- # pages of `Resources;:OrderCancellation`s
74
+ # pages of `Resources::OrderCancellation`s.
75
+ #
76
+ # By default, this will use pages of 200 results under the hood, but this
77
+ # can be customised by specifying the `:limit` option in the `:params`.
75
78
  #
76
79
  # @param options [Hash] options passed to `#list`, for example `:params` to
77
80
  # send an HTTP querystring with filters
78
81
  # @return [Enumerator]
79
82
  # @raise [Errors::Error] when the Duffel API returns an error
80
83
  def all(options = {})
84
+ options[:params] = DEFAULT_ALL_PARAMS.merge(options[:params] || {})
85
+
81
86
  Paginator.new(
82
87
  service: self,
83
88
  options: options,
@@ -22,13 +22,18 @@ module DuffelAPI
22
22
  end
23
23
 
24
24
  # Returns an `Enumerator` which can automatically cycle through multiple
25
- # pages of `Resources;:OrderChangeOffer`s
25
+ # pages of `Resources::OrderChangeOffer`s.
26
+ #
27
+ # By default, this will use pages of 200 results under the hood, but this
28
+ # can be customised by specifying the `:limit` option in the `:params`.
26
29
  #
27
30
  # @param options [Hash] options passed to `#list`, for example `:params` to
28
31
  # send an HTTP querystring with filters
29
32
  # @return [Enumerator]
30
33
  # @raise [Errors::Error] when the Duffel API returns an error
31
34
  def all(options = {})
35
+ options[:params] = DEFAULT_ALL_PARAMS.merge(options[:params] || {})
36
+
32
37
  Paginator.new(
33
38
  service: self,
34
39
  options: options,
@@ -70,13 +70,18 @@ module DuffelAPI
70
70
  end
71
71
 
72
72
  # Returns an `Enumerator` which can automatically cycle through multiple
73
- # pages of `Resources;:Order`s
73
+ # pages of `Resources::Order`s.
74
+ #
75
+ # By default, this will use pages of 200 results under the hood, but this
76
+ # can be customised by specifying the `:limit` option in the `:params`.
74
77
  #
75
78
  # @param options [Hash] options passed to `#list`, for example `:params` to
76
79
  # send an HTTP querystring with filters
77
80
  # @return [Enumerator]
78
81
  # @raise [Errors::Error] when the Duffel API returns an error
79
82
  def all(options = {})
83
+ options[:params] = DEFAULT_ALL_PARAMS.merge(options[:params] || {})
84
+
80
85
  Paginator.new(
81
86
  service: self,
82
87
  options: options,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DuffelAPI
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/duffel_api.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "duffel_api/errors/invalid_state_error"
10
10
  require_relative "duffel_api/errors/rate_limit_error"
11
11
  require_relative "duffel_api/errors/validation_error"
12
12
  require_relative "duffel_api/middlewares/raise_duffel_errors"
13
+ require_relative "duffel_api/middlewares/rate_limiter"
13
14
  require_relative "duffel_api/resources/base_resource"
14
15
  require_relative "duffel_api/resources/aircraft"
15
16
  require_relative "duffel_api/resources/airline"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duffel_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Duffel team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-11 00:00:00.000000000 Z
11
+ date: 2022-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base16
@@ -65,7 +65,6 @@ executables: []
65
65
  extensions: []
66
66
  extra_rdoc_files: []
67
67
  files:
68
- - ".DS_Store"
69
68
  - ".gitignore"
70
69
  - ".rspec"
71
70
  - ".rubocop.yml"
@@ -99,6 +98,7 @@ files:
99
98
  - lib/duffel_api/errors/validation_error.rb
100
99
  - lib/duffel_api/list_response.rb
101
100
  - lib/duffel_api/middlewares/raise_duffel_errors.rb
101
+ - lib/duffel_api/middlewares/rate_limiter.rb
102
102
  - lib/duffel_api/paginator.rb
103
103
  - lib/duffel_api/request.rb
104
104
  - lib/duffel_api/resources/aircraft.rb
data/.DS_Store DELETED
Binary file