fulfil-io 0.7.0 → 0.8.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: 63d67b77df97810e72d56c2cd60a859004ee97de438bd11332cb643c8b8d0466
4
- data.tar.gz: 82708a84ce96c3254236ab4ce7e05a9d64477dc12b94ebb47d758ad267cc818e
3
+ metadata.gz: 5e82498f50af6f27434982cbeb6bb3faec54a085e999f4d86293d4378afb801a
4
+ data.tar.gz: d2596d9afedaf2f67aa3f573c139d1c5a03b706e638fdf2407fc482f25336d6f
5
5
  SHA512:
6
- metadata.gz: 609049e9604f06602526be98da080e33bdbc1a1d5a46e4d338cc5a86cf5f9df9584963b6462e30da5d493b354bb73ca7c489d1c9229b37230ef7654af821cc43
7
- data.tar.gz: f27513ccae674a68a294e12aacecf82c92a2caa6d65077a6abc7497c41703b7b689a4cb2a1b421a9b65c95584243d5a9d95b2f33b11571ec76eba009005cce13
6
+ metadata.gz: 67ad033ab2a666abc04747a37e1d4859690158cfdf1c6455cdc3e3383ccfd6aa74b556def1b02e7584945120b426357ed4a85fa1f1feaa74ad118fbe6161280c
7
+ data.tar.gz: 1ade1a109a650b56c76fcaea49ed71d92b87d8f87e6659748ea5c739d6e6f1715ec6099835f2faefd7c515e8482054d580c17b0fcb74342c2cc214d40fabaeee
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  [![Tests](https://github.com/knowndecimal/fulfil/actions/workflows/tests.yml/badge.svg)](https://github.com/knowndecimal/fulfil/actions/workflows/tests.yml)
2
+ [![Maintainability](https://api.codeclimate.com/v1/badges/c6100940d3debd3a3a7c/maintainability)](https://codeclimate.com/github/knowndecimal/fulfil/maintainability)
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/c6100940d3debd3a3a7c/test_coverage)](https://codeclimate.com/github/knowndecimal/fulfil/test_coverage)
2
4
 
3
5
  # Fulfil.io Rubygem
4
6
 
@@ -146,6 +148,8 @@ $ Fulfil.rate_limit.resets_at
146
148
  => #<DateTime: 2022-01-21T16:36:01-04:00 />
147
149
  ```
148
150
 
151
+ ### Automatic retry API call after rate limit hit
152
+
149
153
  Automatic retries are supported whenever the rate limit is reached. However, it's not enabled by default. To enable it, set the `retry_on_rate_limit` to `true`. By default, the request will be retried in 1 second.
150
154
 
151
155
  ```ruby
@@ -157,6 +161,32 @@ Fulfil.configure do |config|
157
161
  end
158
162
  ```
159
163
 
164
+ ### Monitor rate limit hits
165
+
166
+ Through the configurable `rate_limit_notification_handler` one can monitor the rate limit hits to the APM tool of choice.
167
+
168
+ ```ruby
169
+ # config/initializers/fulfil.rb
170
+
171
+ Fulfil.configure do |config|
172
+ config.rate_limit_notification_handler = proc {
173
+ FakeAPM.increment_counter('fulfil.rate_limit_exceeded')
174
+ }
175
+ end
176
+ ```
177
+
178
+ ## Retrieve multiple records
179
+
180
+ To retrieve multiple records at once, one can pass the IDs into the find method directly.
181
+
182
+ ```ruby
183
+ FulfilClient.find(
184
+ model: 'sale.sale',
185
+ ids: [1, 2, 3, 4],
186
+ fields: %w[id status]
187
+ )
188
+ ```
189
+
160
190
  ## Development
161
191
 
162
192
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -197,7 +227,7 @@ def test_find_one
197
227
  stub_fulfil_get('sale.sale/213112', 'sale_sale')
198
228
 
199
229
  client = Fulfil::Client.new
200
- response = client.find_one(model: 'sale.sale', id: 213_112)
230
+ response = client.find(model: 'sale.sale', id: 213_112)
201
231
 
202
232
  assert_equal 213_112, response['id']
203
233
  end
data/Rakefile CHANGED
@@ -1,10 +1,12 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
3
5
 
4
6
  Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/*_test.rb']
8
10
  end
9
11
 
10
- task :default => :test
12
+ task default: :test
data/lib/fulfil/client.rb CHANGED
@@ -5,8 +5,8 @@ require 'logger'
5
5
  require 'fulfil/response_parser'
6
6
 
7
7
  module Fulfil
8
- SUBDOMAIN = ENV['FULFIL_SUBDOMAIN']
9
- API_KEY = ENV['FULFIL_API_KEY']
8
+ SUBDOMAIN = ENV.fetch('FULFIL_SUBDOMAIN', nil)
9
+ API_KEY = ENV.fetch('FULFIL_API_KEY', nil)
10
10
 
11
11
  class Client
12
12
  class InvalidClientError < StandardError
@@ -93,6 +93,7 @@ module Fulfil
93
93
  uri = URI(model_url(model: model, id: id, endpoint: endpoint))
94
94
 
95
95
  result = request(verb: :put, endpoint: uri, json: body)
96
+
96
97
  parse(result: result)
97
98
  end
98
99
 
@@ -114,10 +115,10 @@ module Fulfil
114
115
  def oauth_token
115
116
  if ENV['FULFIL_TOKEN']
116
117
  puts "You're using an deprecated environment variable. Please update your " \
117
- 'FULFIL_TOKEN to FULFIL_OAUTH_TOKEN.'
118
+ 'FULFIL_TOKEN to FULFIL_OAUTH_TOKEN.'
118
119
  end
119
120
 
120
- ENV['FULFIL_OAUTH_TOKEN'] || ENV['FULFIL_TOKEN']
121
+ ENV['FULFIL_OAUTH_TOKEN'] || ENV.fetch('FULFIL_TOKEN', nil)
121
122
  end
122
123
 
123
124
  def parse(result: nil, results: [])
@@ -174,10 +175,9 @@ module Fulfil
174
175
  end
175
176
 
176
177
  def client
177
- client = HTTP.use(logging: @debug ? { logger: Logger.new(STDOUT) } : {})
178
+ client = HTTP.use(logging: @debug ? { logger: Logger.new($stdout) } : {})
178
179
  client = client.auth("Bearer #{@token}") if @token
179
- client = client.headers(@headers)
180
- client
180
+ client.headers(@headers)
181
181
  end
182
182
 
183
183
  def config
@@ -9,6 +9,19 @@ module Fulfil
9
9
  attr_accessor :retry_on_rate_limit
10
10
  attr_accessor :retry_on_rate_limit_wait
11
11
 
12
+ # Allows the client to configure a notification handler. Can be used by APM
13
+ # tools to monitor the number of rate limit hits.
14
+ #
15
+ # @example Use APM to monitor the API rate limit hits
16
+ # Fulfil.configure do |config|
17
+ # config.rate_limit_notification_handler = proc {
18
+ # FakeAPM.increment_counter('fulfil.rate_limit_exceeded')
19
+ # }
20
+ # end
21
+ #
22
+ # @return [Proc, nil]
23
+ attr_accessor :rate_limit_notification_handler
24
+
12
25
  def initialize
13
26
  @retry_on_rate_limit = false
14
27
  @retry_on_rate_limit_wait = 1
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fulfil
2
4
  class InteractiveReport
3
5
  def initialize(client:, report:)
@@ -37,4 +39,4 @@ module Fulfil
37
39
  }
38
40
  end
39
41
  end
40
- end
42
+ end
data/lib/fulfil/model.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fulfil/query'
2
4
 
3
5
  module Fulfil
@@ -12,15 +14,15 @@ module Fulfil
12
14
 
13
15
  # Delegate this to the client, including the model_name so we don't have to
14
16
  # type it every time.
15
- def find(model: model_name, id:)
17
+ def find(id:, model: model_name)
16
18
  @client.find(model: model, id: id)
17
19
  end
18
20
 
19
21
  # Delegate this to the client, including the model_name so we don't have to
20
22
  # type it every time.
21
23
  def search(
22
- model: model_name,
23
24
  domain:,
25
+ model: model_name,
24
26
  fields: %w[id rec_name],
25
27
  limit: nil,
26
28
  offset: nil,
@@ -51,7 +53,7 @@ module Fulfil
51
53
 
52
54
  def attributes
53
55
  results = @client.search(model: model_name, domain: [], limit: 1)
54
- @client.find(model: model_name, id: results.first.dig('id'))
56
+ @client.find(model: model_name, id: results.first['id'])
55
57
  end
56
58
 
57
59
  def fetch_associated(models:, association_name:, source_key:, fields:)
@@ -66,7 +68,7 @@ module Fulfil
66
68
  model: association_name, ids: associated_ids, fields: fields
67
69
  )
68
70
 
69
- associated_models_by_id = associated_models.map { |m| [m['id'], m] }.to_h
71
+ associated_models_by_id = associated_models.to_h { |m| [m['id'], m] }
70
72
 
71
73
  models.each do |model|
72
74
  filtered_models =
data/lib/fulfil/query.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fulfil
2
4
  class Query
3
5
  def initialize
@@ -9,11 +11,12 @@ module Fulfil
9
11
  end
10
12
 
11
13
  def search(*args)
12
- options = args.first { |arg| arg.is_a?(Hash) && arg.keys.include?(:options) }.fetch(:options, {})
14
+ options = args.first { |arg| arg.is_a?(Hash) && arg.key?(:options) }.fetch(:options, {})
13
15
 
14
16
  args.each do |arg|
15
17
  arg.each do |field, value|
16
18
  next if value == options
19
+
17
20
  @matchers.concat(build_search_term(field: field, value: value, options: options))
18
21
  end
19
22
  end
@@ -22,17 +25,18 @@ module Fulfil
22
25
  end
23
26
 
24
27
  def exclude(*args)
25
- options = args.first { |arg| arg.is_a?(Hash) && arg.keys.include?(:options) }.fetch(:options, {})
28
+ options = args.first { |arg| arg.is_a?(Hash) && arg.key?(:options) }.fetch(:options, {})
26
29
 
27
30
  terms = args.flat_map do |arg|
28
- arg.map do |field, value|
29
- next if value == options
30
- build_exclude_term(field: field, value: value, options: options)
31
- end
32
- end
31
+ arg.map do |field, value|
32
+ next if value == options
33
+
34
+ build_exclude_term(field: field, value: value, options: options)
35
+ end
36
+ end
33
37
 
34
38
  if terms.length > 1
35
- @matchers.push(["OR"].concat(terms))
39
+ @matchers.push(['OR'].concat(terms))
36
40
  else
37
41
  @matchers.concat(terms.first)
38
42
  end
@@ -62,7 +66,7 @@ module Fulfil
62
66
  #
63
67
  # IN, NOT IN: (Array)
64
68
  #
65
- def build_search_term(prefix: nil, field:, value:, options:)
69
+ def build_search_term(field:, value:, options:, prefix: nil)
66
70
  key = [prefix, field.to_s].compact.join('.')
67
71
 
68
72
  case value.class.name
@@ -73,7 +77,7 @@ module Fulfil
73
77
  when 'Range'
74
78
  [
75
79
  [key, '>=', value.first],
76
- [key, '<=', value.last],
80
+ [key, '<=', value.last]
77
81
  ]
78
82
  when 'String'
79
83
  if options[:case_sensitive]
@@ -82,15 +86,15 @@ module Fulfil
82
86
  [[key, 'ilike', value]]
83
87
  end
84
88
  when 'Hash'
85
- value.flat_map { |nested_field, nested_value|
89
+ value.flat_map do |nested_field, nested_value|
86
90
  build_search_term(prefix: field, field: nested_field, value: nested_value, options: options)
87
- }
91
+ end
88
92
  else
89
93
  raise "Unhandled value type: #{value} (#{value.class.name})"
90
94
  end
91
95
  end
92
96
 
93
- def build_exclude_term(prefix: nil, field:, value:, options:)
97
+ def build_exclude_term(field:, value:, options:, prefix: nil)
94
98
  key = [prefix, field.to_s].compact.join('.')
95
99
 
96
100
  case value.class.name
@@ -101,12 +105,12 @@ module Fulfil
101
105
  when 'Range'
102
106
  [
103
107
  [key, '<', value.first],
104
- [key, '>', value.last],
108
+ [key, '>', value.last]
105
109
  ]
106
110
  when 'Hash'
107
- value.flat_map { |nested_field, nested_value|
111
+ value.flat_map do |nested_field, nested_value|
108
112
  build_exclude_term(prefix: field, field: nested_field, value: nested_value, options: options)
109
- }
113
+ end
110
114
  else
111
115
  raise "Unhandled value type: #{value} (#{value.class.name})"
112
116
  end
@@ -8,7 +8,8 @@ module Fulfil
8
8
 
9
9
  # Analyses the rate limit based on the response headers from Fulfil.
10
10
  # @param headers [HTTP::Headers] The HTTP response headers from Fulfil.
11
- # @return [Fulfil::RateLimit]
11
+ # @raise [Fulfil::RateLimitExceeded] When the rate limit is hit.
12
+ # @return [true]
12
13
  def analyse!(headers)
13
14
  rate_limit_headers = RateLimitHeaders.new(headers)
14
15
 
@@ -16,7 +17,9 @@ module Fulfil
16
17
  self.requests_left = rate_limit_headers.requests_left
17
18
  self.resets_at = rate_limit_headers.resets_at
18
19
 
19
- raise Fulfil::RateLimitExceeded unless requests_left?
20
+ return true if requests_left?
21
+
22
+ report_rate_limit_hit_and_raise
20
23
  end
21
24
 
22
25
  # Returns whether there are any requests left in the current rate limit window.
@@ -24,5 +27,13 @@ module Fulfil
24
27
  def requests_left?
25
28
  requests_left&.positive?
26
29
  end
30
+
31
+ private
32
+
33
+ # @raise [Fulfil::RateLimitExceeded]
34
+ def report_rate_limit_hit_and_raise
35
+ Fulfil.config.rate_limit_notification_handler&.call
36
+ raise Fulfil::RateLimitExceeded
37
+ end
27
38
  end
28
39
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Fulfil
2
4
  module ResponseParser
3
5
  class UnhandledTypeError < StandardError
@@ -17,19 +19,19 @@ module Fulfil
17
19
  # }
18
20
  #
19
21
  def self.mapped_value_field(value:)
20
- return value unless value.is_a?(Hash) && value.dig('__class__')
22
+ return value unless value.is_a?(Hash) && value['__class__']
21
23
 
22
- json_class = value.dig('__class__')
24
+ json_class = value['__class__']
23
25
 
24
26
  case json_class
25
27
  when 'date'
26
- date = value.dig('iso_string')
28
+ date = value['iso_string']
27
29
  Date.parse(date)
28
30
  when 'datetime'
29
- time = value.dig('iso_string')
31
+ time = value['iso_string']
30
32
  DateTime.parse(time)
31
33
  when 'Decimal', 'timedelta'
32
- value.dig('decimal').to_f
34
+ value['decimal'].to_f
33
35
  else
34
36
  raise UnhandledTypeError.new(
35
37
  "received a value that we don't know how to handle: #{json_class}",
@@ -46,15 +48,19 @@ module Fulfil
46
48
  [group_key, mapped_value_field(value: kv_tuples[0][1])]
47
49
  else
48
50
  id = kv_tuples[0]
49
- attrs = kv_tuples[1..-1].map { |tuple| [tuple[0][1..-1], tuple[1]] }
51
+ attrs = kv_tuples[1..].map { |tuple| [tuple[0][1..], tuple[1]] }
50
52
  [group_key, [['id', id[1]]].concat(group(attrs)).to_h]
51
53
  end
52
54
  end
53
55
  end
54
56
 
55
57
  def self.parse(item:)
56
- key_value_tuples = item.to_a.map { |item_tuple| [item_tuple[0].split('.'), item_tuple[1]] }
57
- group(key_value_tuples).to_h
58
+ case item
59
+ when Hash
60
+ group(item.map { |(key, value)| [key.split('.'), value] }).to_h
61
+ else
62
+ item
63
+ end
58
64
  end
59
65
  end
60
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fulfil
4
- VERSION = "0.7.0"
4
+ VERSION = '0.8.0'
5
5
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fulfil-io
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Moore
8
8
  - Kat Fairbanks
9
+ - Stefan Vermaas
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2022-09-22 00:00:00.000000000 Z
13
+ date: 2023-04-20 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: http
@@ -20,7 +21,7 @@ dependencies:
20
21
  version: 4.4.1
21
22
  - - "<"
22
23
  - !ruby/object:Gem::Version
23
- version: 5.1.0
24
+ version: 5.2.0
24
25
  type: :runtime
25
26
  prerelease: false
26
27
  version_requirements: !ruby/object:Gem::Requirement
@@ -30,111 +31,7 @@ dependencies:
30
31
  version: 4.4.1
31
32
  - - "<"
32
33
  - !ruby/object:Gem::Version
33
- version: 5.1.0
34
- - !ruby/object:Gem::Dependency
35
- name: bundler
36
- requirement: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '2.0'
41
- type: :development
42
- prerelease: false
43
- version_requirements: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: '2.0'
48
- - !ruby/object:Gem::Dependency
49
- name: minitest
50
- requirement: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '5.0'
55
- type: :development
56
- prerelease: false
57
- version_requirements: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '5.0'
62
- - !ruby/object:Gem::Dependency
63
- name: minitest-reporters
64
- requirement: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '1.3'
69
- type: :development
70
- prerelease: false
71
- version_requirements: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.3'
76
- - !ruby/object:Gem::Dependency
77
- name: oauth2
78
- requirement: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.4'
83
- type: :development
84
- prerelease: false
85
- version_requirements: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '1.4'
90
- - !ruby/object:Gem::Dependency
91
- name: rake
92
- requirement: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- type: :development
98
- prerelease: false
99
- version_requirements: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- - !ruby/object:Gem::Dependency
105
- name: webmock
106
- requirement: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- type: :development
112
- prerelease: false
113
- version_requirements: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
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
34
+ version: 5.2.0
138
35
  description:
139
36
  email:
140
37
  - chris@knowndecimal.com
@@ -165,7 +62,8 @@ files:
165
62
  homepage: https://github.com/knowndecimal/fulfil
166
63
  licenses:
167
64
  - MIT
168
- metadata: {}
65
+ metadata:
66
+ rubygems_mfa_required: 'true'
169
67
  post_install_message:
170
68
  rdoc_options: []
171
69
  require_paths:
@@ -174,7 +72,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
174
72
  requirements:
175
73
  - - ">="
176
74
  - !ruby/object:Gem::Version
177
- version: '2.4'
75
+ version: '2.6'
178
76
  required_rubygems_version: !ruby/object:Gem::Requirement
179
77
  requirements:
180
78
  - - ">="