shopify_api-graphql-tiny 0.1.1 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 125818846e86b77345ee59d8e6d03c4600b2aaa56f18e33ec1ae23ecfa90938e
4
- data.tar.gz: 4b4443e43ff6d37fdbbad202222b6f5b3dff11f8f0a377260898dab06d2209ec
3
+ metadata.gz: c827a0b468aeb34cfb05760a7a1a25e641c61f21f6d2d15c4ed8a05b29bd383b
4
+ data.tar.gz: 23b357593431fb8beda6a29689b6b90e4101fde3cac2eaaad38c7a2cb48f1437
5
5
  SHA512:
6
- metadata.gz: 5418a19e0a24b56d7e587c9a1bde22bf4d1c830639393fa30b371216dc6e8f9824cf1b31f477e6834ae17ec92c0594803a8b0593920a1b7db22655e5fc8439dd
7
- data.tar.gz: b2ae36edf66dcb67395950e39231cbba3d13f0873b381a78b1cd8404f3e40fd3c7dbc157030c6665cc029ea6800611455cfb19da8fc5dfcd7fc08c3d45304631
6
+ metadata.gz: 7fe6d5f27b0936703ce4f7f45875406ab11069d41f8e83e9d57a873ce5dd63d1093e88219cd8b5b69eed2bdd3b39bb233292360aa8bb4534f5a799690cceab5f
7
+ data.tar.gz: d7231b2be188d9bedbfa92f6de69a0ee4c082b8801633db73c570c7cef6c6a349afc81e942e2f36b685edf26a02d9e74817595d7b4988298f42092182a49a718
data/.env.template CHANGED
@@ -4,11 +4,13 @@
4
4
  # 2. Fill-in the below variables with valid values.
5
5
  # These values will be used by the tests. Data will be written to SHOPIFY_DOMAIN
6
6
  #
7
+
8
+ # Must be full domain as it's used in test assertions
7
9
  SHOPIFY_DOMAIN=
8
10
  SHOPIFY_TOKEN=
9
11
 
10
- # Will result in the creation of a private metadafield
12
+ # Will result in the creation of a metadafield on the customer. write_customers permission required
11
13
  SHOPIFY_CUSTOMER_ID=
12
14
 
13
- # Must have more than 1 variant
15
+ # Must have more than 1 variant values
14
16
  SHOPIFY_PRODUCT_ID=
@@ -15,10 +15,10 @@ jobs:
15
15
 
16
16
  strategy:
17
17
  matrix:
18
- ruby: ["3.2", "3.1", "3.0", "2.7.2", "2.6.6", "2.5.8", "2.4.10"]
18
+ ruby: ['3.4', '3.3', '3.2', '3.1', '3.0', '2.7']
19
19
 
20
20
  steps:
21
- - uses: actions/checkout@v2
21
+ - uses: actions/checkout@v4
22
22
  - uses: ruby/setup-ruby@v1
23
23
  with:
24
24
  bundler-cache: true
data/Changes CHANGED
@@ -1,3 +1,12 @@
1
+ 2026-01-20 v1.0.0
2
+ --------------------
3
+ * Add support for exponential backoff and remove ShopifyAPIRetry
4
+ * Support HTTP status and Shopify error codes as retry options
5
+
6
+ 2026-01-20 v0.2.0
7
+ --------------------
8
+ * Deprecate :retry with a Hash value
9
+
1
10
  2023-02-12 v0.1.1
2
11
  --------------------
3
12
  * Add pagination support
data/README.md CHANGED
@@ -1,9 +1,29 @@
1
1
  # ShopifyAPI::GraphQL::Tiny
2
2
 
3
- Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry.
3
+ Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry
4
4
 
5
5
  [![CI](https://github.com/ScreenStaring/shopify_api-graphql-tiny/actions/workflows/ci.yml/badge.svg)](https://github.com/ScreenStaring/shopify_api-graphql-tiny/actions)
6
6
 
7
+ ## Installation
8
+
9
+ Add this line to your application's `Gemfile`:
10
+
11
+ ```ruby
12
+ gem "shopify_api-graphql-tiny"
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```sh
18
+ bundle
19
+ ```
20
+
21
+ Or install it yourself as:
22
+
23
+ ```sh
24
+ gem install shopify_api-graphql-tiny
25
+ ```
26
+
7
27
  ## Usage
8
28
 
9
29
  ```rb
@@ -54,6 +74,69 @@ GQL
54
74
  p result.dig("data", "customerUpdate", "userErrors")
55
75
  ```
56
76
 
77
+ ### Automatically Retrying Failed Requests
78
+
79
+ There are 2 types of retries: 1) request is rate-limited by Shopify 2) request fails due to an exception or non-200 HTTP response.
80
+
81
+ When a request is rate-limited by Shopify retry occurs according to [Shopify's `throttleStatus`](https://shopify.dev/docs/api/admin-graphql/unstable#rate-limits)
82
+
83
+ When a request fails due to an exception or non-200 HTTP status a retry will be attempted after an exponential backoff waiting period.
84
+ This is controlled by `ShopifyAPI::GraphQL::Tiny::DEFAULT_BACKOFF_OPTIONS`. It contains:
85
+
86
+ * `:base_delay` - `0.5`
87
+ * `:jitter` - `true`
88
+ * `:max_attempts` - `10`
89
+ * `:max_delay` - `60`
90
+ * `:multiplier` - `2.0`
91
+
92
+ `:max_attempts` dictates how many retry attempts will be made **for all** types of retries.
93
+
94
+ These can be overridden globally (by assigning to the constant) or per instance:
95
+
96
+ ```rb
97
+ gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :max_attempts => 20, :max_delay => 90)
98
+ ```
99
+
100
+ `ShopifyAPI::GraphQL::Tiny::DEFAULT_RETRY_ERRORS` determines what is retried. It contains and HTTP statuses codes, Shopify GraphQL errors codes, and exceptions.
101
+ By default it contains:
102
+
103
+ * `"5XX"` - Any HTTP 5XX status
104
+ * `"INTERNAL_SERVER_ERROR"` - Shopify GraphQL error code
105
+ * `"TIMEOUT"` - Shopify GraphQL error code
106
+ * `EOFError`
107
+ * `Errno::ECONNABORTED`
108
+ * `Errno::ECONNREFUSED`
109
+ * `Errno::ECONNRESET`
110
+ * `Errno::EHOSTUNREACH`
111
+ * `Errno::EINVAL`
112
+ * `Errno::ENETUNREACH`
113
+ * `Errno::ENOPROTOOPT`
114
+ * `Errno::ENOTSOCK`
115
+ * `Errno::EPIPE`
116
+ * `Errno::ETIMEDOUT`
117
+ * `Net::HTTPBadResponse`
118
+ * `Net::HTTPHeaderSyntaxError`
119
+ * `Net::ProtocolError`
120
+ * `Net::ReadTimeout`
121
+ * `OpenSSL::SSL::SSLError`
122
+ * `SocketError`
123
+ * `Timeout::Error`
124
+
125
+ These can be overridden globally (by assigning to the constant) or per instance:
126
+
127
+ ```rb
128
+ # Only retry on 2 errors
129
+ gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :retry => [SystemCallError, "500"])
130
+ ```
131
+
132
+ #### Disabling Automatic Retry
133
+
134
+ To disable retries set the `:retry` option to `false`:
135
+
136
+ ```rb
137
+ gql = ShopifyAPI::GraphQL::Tiny.new(shop, token, :retry => false)
138
+ ```
139
+
57
140
  ### Pagination
58
141
 
59
142
  In addition to built-in request retry `ShopifyAPI::GraphQL::Tiny` also builds in support for pagination.
@@ -64,6 +147,8 @@ in your queries and wrap them in a function that accepts a page/cursor argument.
64
147
  The pager's `#execute` is like the non-paginated `#execute` method and accepts additional, non-pagination query arguments:
65
148
 
66
149
  ```rb
150
+ gql = ShopifyAPI::GraphQL::Tiny.new("my-shop", token)
151
+ pager = gql.paginate
67
152
  pager.execute(query, :foo => 123)
68
153
  ```
69
154
 
@@ -174,19 +259,21 @@ pager.execute(query) { |page| }
174
259
 
175
260
  The `"data"` and `"pageInfo"` keys are automatically added if not provided.
176
261
 
177
- ### Automatically Retrying Failed Requests
178
-
179
- See [the docs](https://rubydoc.info/gems/shopify_api-graphql-tiny) for more information.
180
-
181
262
  ## Testing
182
263
 
183
264
  `cp env.template .env` and fill-in `.env` with the missing values. This requires a Shopify store.
184
265
 
266
+ To elicit a request that will be rate-limited by Shopify run following Rake task:
267
+
268
+ ```sh
269
+ bundle exec rake rate_limit SHOPIFY_DOMAIN=your-domain SHOPIFY_TOKEN=your-token
270
+ ```
271
+
185
272
  ## See Also
186
273
 
187
274
  - [Shopify Dev Tools](https://github.com/ScreenStaring/shopify-dev-tools) - Command-line program to assist with the development and/or maintenance of Shopify apps and stores
188
- - [Shopify ID Export](https://github.com/ScreenStaring/shopify_id_export/) Dump Shopify product and variant IDs —along with other identifiers— to a CSV or JSON file
189
- - [ShopifyAPIRetry](https://github.com/ScreenStaring/shopify_api_retry) - Retry a ShopifyAPI request if rate-limited or other errors occur (REST and GraphQL APIs)
275
+ - [Shopify ID Export](https://github.com/ScreenStaring/shopify_id_export/) - Dump Shopify product and variant IDs —along with other identifiers— to a CSV or JSON file
276
+ - [`TinyGID`](https://github.com/sshaw/tiny_gid/) - Build Global ID (gid://) URI strings from scalar values
190
277
 
191
278
  ## License
192
279
 
data/Rakefile CHANGED
@@ -4,3 +4,161 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :default => :spec
7
+
8
+ desc "Elicit a Shopify rate limit"
9
+ task :rate_limit do
10
+ require "shopify_api/graphql/tiny"
11
+
12
+ query =<<-GQL
13
+ query {
14
+ products(first: 50, sortKey: UPDATED_AT, reverse: true) {
15
+ pageInfo {
16
+ hasNextPage
17
+ endCursor
18
+ }
19
+ edges {
20
+ node {
21
+ id
22
+ title
23
+ handle
24
+ status
25
+ createdAt
26
+ updatedAt
27
+ publishedAt
28
+ vendor
29
+ productType
30
+ tags
31
+ descriptionHtml
32
+ description
33
+ onlineStoreUrl
34
+ options(first: 3) {
35
+ id
36
+ name
37
+ position
38
+ values
39
+ }
40
+ variants(first: 80) {
41
+ edges {
42
+ node {
43
+ id
44
+ title
45
+ price
46
+ compareAtPrice
47
+ sku
48
+ barcode
49
+ inventoryItem {
50
+ id
51
+ unitCost {
52
+ amount
53
+ currencyCode
54
+ }
55
+ countryCodeOfOrigin
56
+ harmonizedSystemCode
57
+ }
58
+ inventoryPolicy
59
+ taxable
60
+ availableForSale
61
+ metafields(first: 20) {
62
+ edges {
63
+ node {
64
+ id
65
+ namespace
66
+ key
67
+ value
68
+ type
69
+ description
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+ media(first: 20) {
77
+ edges {
78
+ node {
79
+ __typename
80
+ alt
81
+ status
82
+ ... on MediaImage {
83
+ id
84
+ preview {
85
+ image {
86
+ url
87
+ }
88
+ }
89
+ image {
90
+ id
91
+ url
92
+ width
93
+ height
94
+ altText
95
+ }
96
+ }
97
+ ... on Video {
98
+ id
99
+ sources {
100
+ url
101
+ format
102
+ height
103
+ width
104
+ }
105
+ }
106
+ ... on ExternalVideo {
107
+ id
108
+ originUrl
109
+ embedUrl
110
+ }
111
+ }
112
+ }
113
+ }
114
+ images(first: 20) {
115
+ edges {
116
+ node {
117
+ id
118
+ url
119
+ altText
120
+ width
121
+ height
122
+ }
123
+ }
124
+ }
125
+ seo {
126
+ title
127
+ description
128
+ }
129
+ metafields(first: 30) {
130
+ edges {
131
+ node {
132
+ id
133
+ namespace
134
+ key
135
+ value
136
+ type
137
+ ownerType
138
+ }
139
+ }
140
+ }
141
+ collections(first: 10) {
142
+ edges {
143
+ node {
144
+ id
145
+ title
146
+ handle
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ GQL
155
+
156
+ threads = 5.times.map do
157
+ Thread.new do
158
+ gql = ShopifyAPI::GQL::Tiny.new(ENV.fetch("SHOPIFY_DOMAIN"), ENV.fetch("SHOPIFY_TOKEN"), :debug => true)
159
+ pp gql.execute(query).dig("extensions", "cost", "throttleStatus")
160
+ end
161
+ end
162
+
163
+ threads.each(&:join)
164
+ end
@@ -3,7 +3,7 @@
3
3
  module ShopifyAPI
4
4
  module GraphQL
5
5
  class Tiny
6
- VERSION = "0.1.1"
6
+ VERSION = "1.0.0"
7
7
  end
8
8
  end
9
9
  end
@@ -2,18 +2,22 @@
2
2
  require "json"
3
3
  require "net/http"
4
4
 
5
- require "shopify_api_retry"
5
+ require "net_http_timeout_errors"
6
6
  require "shopify_api/graphql/tiny/version"
7
7
 
8
8
  module ShopifyAPI
9
9
  module GraphQL
10
10
  ##
11
- # Client to make Shopify GraphQL Admin API requests with built-in retries.
11
+ # Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in pagination and retry
12
12
  #
13
13
  class Tiny
14
14
  Error = Class.new(StandardError)
15
15
  ConnectionError = Class.new(Error)
16
16
 
17
+ ERROR_CODE_THROTTLED = "THROTTLED"
18
+ ERROR_CODE_TIMEOUT = "TIMEOUT"
19
+ ERROR_CODE_SERVER_ERROR = "INTERNAL_SERVER_ERROR"
20
+
17
21
  class GraphQLError < Error
18
22
  # Hash of failed GraphQL response
19
23
  attr_reader :response
@@ -44,18 +48,26 @@ module ShopifyAPI
44
48
 
45
49
  DEFAULT_HEADERS = { "Content-Type" => "application/json" }.freeze
46
50
 
47
- # Retry rules to be used for all instances if no rules are specified via the +:retry+ option when creating an instance
48
- DEFAULT_RETRY_OPTIONS = {
49
- ConnectionError => { :wait => 3, :tries => 20 },
50
- GraphQLError => { :wait => 3, :tries => 20 },
51
- HTTPError => { :wait => 3, :tries => 20 }
51
+ DEFAULT_BACKOFF_OPTIONS = {
52
+ :base_delay => 0.5,
53
+ :jitter => true,
54
+ :max_attempts => 10,
55
+ :max_delay => 60,
56
+ :multiplier => 2.0
52
57
  }
53
58
 
59
+ DEFAULT_RETRY_ERRORS = [
60
+ "5XX",
61
+ ERROR_CODE_SERVER_ERROR,
62
+ ERROR_CODE_TIMEOUT,
63
+ *NetHttpTimeoutErrors.all
64
+ ]
65
+
54
66
  ENDPOINT = "https://%s/admin/api%s/graphql.json" # Note that we omit the "/" after API for the case where there's no version.
55
67
 
56
68
  ##
57
69
  #
58
- # Create a new GraphQL client.
70
+ # Create a new GraphQL client
59
71
  #
60
72
  # === Arguments
61
73
  #
@@ -65,8 +77,12 @@ module ShopifyAPI
65
77
  #
66
78
  # === Options
67
79
  #
68
- # [:retry (Boolean|Hash)] +Hash+ can be retry config options. For the format see {ShopifyAPIRetry}[https://github.com/ScreenStaring/shopify_api_retry/#usage]. Defaults to +true+
80
+ # [:retry (Boolean|Array)] If +false+ disable retries or an +Array+ of errors to retry. Can be HTTP status codes, GraphQL errors, or exception classes.
69
81
  # [:version (String)] Shopify API version to use. Defaults to the latest version.
82
+ # [:max_attempts (Integer)] Maximum number of retry attempts across all errors. Defaults to +10+
83
+ # [:base_delay (Float)] Exponential backoff base delay. Defaults to +0.5+
84
+ # [:jitter (Boolean)] Exponential backoff jitter (random delay added to backoff). Defaults to +true+
85
+ # [:multiplier (Float)] Exponential backoff multiplier. Defaults to +2.0+
70
86
  # [:debug (Boolean|IO)] Output the HTTP request/response to +STDERR+ or to its value if it's an +IO+. Defaults to +false+.
71
87
  #
72
88
  # === Errors
@@ -83,13 +99,27 @@ module ShopifyAPI
83
99
 
84
100
  @headers = DEFAULT_HEADERS.dup
85
101
  @headers[ACCESS_TOKEN_HEADER] = token
86
- @headers[QUERY_COST_HEADER] = "true" if retry?
102
+ @headers[QUERY_COST_HEADER] = "true" unless @options[:retry] == false
87
103
 
88
104
  @endpoint = URI(sprintf(ENDPOINT, @domain, !@options[:version].to_s.strip.empty? ? "/#{@options[:version]}" : ""))
105
+ @backoff_options = DEFAULT_BACKOFF_OPTIONS.merge(@options.slice(*DEFAULT_BACKOFF_OPTIONS.keys))
106
+
107
+ if @options[:debug]
108
+ @debug = @options[:debug].is_a?(IO) ? @options[:debug] : $stderr
109
+ end
110
+
111
+ case @options[:retry]
112
+ when false
113
+ @retryable = []
114
+ when Array
115
+ @retryable = @options[:retry]
116
+ else
117
+ @retryable = DEFAULT_RETRY_ERRORS
118
+ end
89
119
  end
90
120
 
91
121
  #
92
- # Execute a GraphQL query or mutation.
122
+ # Execute a GraphQL query or mutation
93
123
  #
94
124
  # === Arguments
95
125
  #
@@ -100,6 +130,8 @@ module ShopifyAPI
100
130
  #
101
131
  # ArgumentError, ConnectionError, HTTPError, RateLimitError, GraphQLError
102
132
  #
133
+ # Outside of ArgumentError these are raised after exhausing the configured retry.
134
+ #
103
135
  # * An ShopifyAPI::GraphQL::Tiny::HTTPError is raised of the response does not have 200 status code
104
136
  # * A ShopifyAPI::GraphQL::Tiny::RateLimitError is raised if rate-limited and retries are disabled or if still
105
137
  # rate-limited after the configured number of retry attempts
@@ -113,8 +145,9 @@ module ShopifyAPI
113
145
  def execute(q, variables = nil)
114
146
  raise ArgumentError, "query required" if q.nil? || q.to_s.strip.empty?
115
147
 
116
- config = retry? ? @options[:retry] || DEFAULT_RETRY_OPTIONS : {}
117
- ShopifyAPIRetry::GraphQL.retry(config) { post(q, variables) }
148
+ @request_attempts = 0
149
+
150
+ make_request(q, variables)
118
151
  end
119
152
 
120
153
  ##
@@ -165,57 +198,134 @@ module ShopifyAPI
165
198
 
166
199
  private
167
200
 
168
- def retry?
169
- @options[:retry] != false
170
- end
201
+ def make_request(query, variables = nil)
202
+ response = nil
203
+ exceptions = @retryable.select { |target| target.is_a?(Class) }
171
204
 
172
- def shopify_domain(host)
173
- domain = host.sub(%r{\Ahttps?://}i, "")
174
- domain << SHOPIFY_DOMAIN unless domain.end_with?(SHOPIFY_DOMAIN)
175
- domain
205
+ begin
206
+ @request_attempts += 1
207
+ response = post(query, variables)
208
+ rescue => e
209
+ retry if exceptions.include?(e.class) && wait_to_retry
210
+ raise ConnectionError.new("failed to execute query for #@domain: #{e.message}")
211
+ end
212
+
213
+ if response.code != "200"
214
+ return make_request(query, variables) if handle_http_error(response.code)
215
+ raise HTTPError.new("failed to execute query for #@domain: #{response.body}", response.code)
216
+ end
217
+
218
+ json = parse_json(response.body)
219
+ return json unless json.include?("errors")
220
+
221
+ return make_request(query, variables) if handle_graphql_error(json)
222
+
223
+ message = error_message(json["errors"])
224
+ raise GraphQLError.new("failed to execute query for #@domain: #{message}", json)
176
225
  end
177
226
 
178
227
  def post(query, variables = nil)
179
- begin
180
- # Newer versions of Ruby
181
- # response = Net::HTTP.post(@endpoint, query, @headers)
182
- params = { :query => query }
183
- params[:variables] = variables if variables
228
+ # Newer versions of Ruby:
229
+ # response = Net::HTTP.post(@endpoint, query, @headers)
230
+ params = { :query => query }
231
+ params[:variables] = variables if variables
184
232
 
185
- post = Net::HTTP::Post.new(@endpoint.path)
186
- post.body = params.to_json
187
- post["User-Agent"] = USER_AGENT
233
+ post = Net::HTTP::Post.new(@endpoint.path)
234
+ post.body = params.to_json
235
+ post["User-Agent"] = USER_AGENT
188
236
 
189
- @headers.each { |k,v| post[k] = v }
237
+ @headers.each { |k,v| post[k] = v }
190
238
 
191
- request = Net::HTTP.new(@endpoint.host, @endpoint.port)
192
- request.use_ssl = true
239
+ request = Net::HTTP.new(@endpoint.host, @endpoint.port)
240
+ request.use_ssl = true
241
+ request.set_debug_output(@debug) if @debug
193
242
 
194
- if @options[:debug]
195
- request.set_debug_output(
196
- @options[:debug].is_a?(IO) ? @options[:debug] : $stderr
197
- )
198
- end
243
+ request.start { |http| http.request(post) }
244
+ end
199
245
 
200
- response = request.start { |http| http.request(post) }
201
- rescue => e
202
- raise ConnectionError, "request to #@endpoint failed: #{e}"
246
+ def handle_graphql_error(json)
247
+ errors = json["errors"]
248
+ codes = errors.map { |error| error.dig("extensions", "code") }
249
+
250
+ if codes.include?(ERROR_CODE_THROTTLED)
251
+ return true if wait_for_shopify_retry(json.dig("extensions", "cost"))
252
+
253
+ raise RateLimitError.new(error_message(errors), json)
203
254
  end
204
255
 
205
- # TODO: Even if non-200 check if JSON. See: https://shopify.dev/api/admin-graphql
206
- prefix = "failed to execute query for #@domain: "
207
- raise HTTPError.new("#{prefix}#{response.body}", response.code) if response.code != "200"
256
+ return true if @retryable.any? { |error| codes.include?(error) } && wait_to_retry
208
257
 
209
- json = JSON.parse(response.body)
210
- return json unless json.include?("errors")
258
+ false
259
+ end
211
260
 
212
- errors = json["errors"].map { |e| e["message"] }.join(", ")
213
- if json.dig("errors", 0, "extensions", "code") == "THROTTLED"
214
- raise RateLimitError.new(errors, json) unless retry?
215
- return json
216
- end
261
+ def handle_http_error(status)
262
+ return false unless @retryable.include?(status) || @retryable.any? { |error| error.is_a?(String) && error.size == 3 && error.end_with?("XX") && error[0] == status[0] }
263
+
264
+ wait_to_retry
265
+ end
266
+
267
+ def wait_to_retry
268
+ return false unless request_attempts_remain?
269
+
270
+ backoff(@request_attempts)
271
+
272
+ true
273
+ end
274
+
275
+ def wait_for_shopify_retry(cost)
276
+ return false if cost.nil? || cost["actualQueryCost"] || !request_attempts_remain?
277
+
278
+ status = cost["throttleStatus"]
279
+ time = (cost["requestedQueryCost"] - status["currentlyAvailable"]) / status["restoreRate"]
280
+
281
+ debug("retrying rate-limited request (retry count: #@request_attempts, status: #{status}, sleep: #{time})")
282
+
283
+ sleep(time)
284
+
285
+ true
286
+ end
287
+
288
+ def backoff(attempts)
289
+ delay = @backoff_options[:base_delay] * (@backoff_options[:multiplier] ** (attempts - 1))
290
+ delay = [delay, @backoff_options[:max_delay]].min if @backoff_options[:max_delay]
291
+ delay = rand * delay if @backoff_options[:jitter]
292
+
293
+ debug("backoff sleeping for #{delay}")
294
+
295
+ sleep(delay)
296
+ end
297
+
298
+ def shopify_domain(host)
299
+ domain = host.sub(%r{\Ahttps?://}i, "")
300
+ domain << SHOPIFY_DOMAIN unless domain.end_with?(SHOPIFY_DOMAIN)
301
+ domain
302
+ end
303
+
304
+ def parse_json(json)
305
+ JSON.parse(json)
306
+ rescue JSON::ParserError => e
307
+ raise Error, "failed to parse JSON response: #{e.message}"
308
+ end
309
+
310
+ def error_message(errors)
311
+ errors.map do |e|
312
+ message = e["message"]
313
+
314
+ path = e["path"]
315
+ message << sprintf(" at %s", path.join(".")) if path
316
+
317
+ message
318
+ end.join(", ")
319
+ end
320
+
321
+ def request_attempts_remain?
322
+ @request_attempts < @backoff_options[:max_attempts]
323
+ end
324
+
325
+ def debug(message)
326
+ return unless @debug
217
327
 
218
- raise GraphQLError.new(prefix + errors, json)
328
+ @debug.puts "#{self.class}: #{message}"
219
329
  end
220
330
  end
221
331
 
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
28
28
  "source_code_uri" => "https://github.com/ScreenStaring/shopify_api-graphql-tiny",
29
29
  }
30
30
 
31
- spec.add_dependency "shopify_api_retry", "~> 0.2"
31
+ spec.add_dependency "net_http_timeout_errors"
32
32
  spec.add_development_dependency "webmock", "~> 3.0"
33
33
  spec.add_development_dependency "bundler"
34
34
  spec.add_development_dependency "rake", ">= 12.3.3"
metadata CHANGED
@@ -1,29 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify_api-graphql-tiny
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skye Shaw
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-02-12 00:00:00.000000000 Z
10
+ date: 2026-01-20 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: shopify_api_retry
13
+ name: net_http_timeout_errors
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '0.2'
18
+ version: '0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - "~>"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '0.2'
25
+ version: '0'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: webmock
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -80,7 +79,6 @@ dependencies:
80
79
  - - "~>"
81
80
  - !ruby/object:Gem::Version
82
81
  version: '3.0'
83
- description:
84
82
  email:
85
83
  - skye.shaw@gmail.com
86
84
  executables: []
@@ -109,7 +107,6 @@ metadata:
109
107
  changelog_uri: https://github.com/ScreenStaring/shopify_api-graphql-tiny/blob/master/Changes
110
108
  documentation_uri: https://rubydoc.info/gems/shopify_api-graphql-tiny
111
109
  source_code_uri: https://github.com/ScreenStaring/shopify_api-graphql-tiny
112
- post_install_message:
113
110
  rdoc_options: []
114
111
  require_paths:
115
112
  - lib
@@ -124,8 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
124
121
  - !ruby/object:Gem::Version
125
122
  version: '0'
126
123
  requirements: []
127
- rubygems_version: 3.1.4
128
- signing_key:
124
+ rubygems_version: 3.6.2
129
125
  specification_version: 4
130
126
  summary: Lightweight, no-nonsense, Shopify GraphQL Admin API client with built-in
131
127
  pagination and retry