internetdata 2.3.1 → 2.3.2

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: 2733fa2507c4610501c4941ee66bd085f4eecee78491803742d7c796de693df2
4
- data.tar.gz: af87a305689fbdba6d7c409944c6b7eeb8f7682ce9e1209829186b7b89a3faf7
3
+ metadata.gz: 75d59c125ee280d0f8d9fe9522fa6003ac9a1e6fc764839f93f2907f3ee7d13f
4
+ data.tar.gz: 9ee66403a693539439df78eb8cb65306f84093fd6480a92361650b7cd6bc987f
5
5
  SHA512:
6
- metadata.gz: 6c4e6ac56b6baad950705defb63fad84e7f85f623cbbff479fc7702d4be222dc9c425e842d4344dbbe40e3e08dbad020c9b7b78b52565459be226b78742b0dec
7
- data.tar.gz: 0a3609002009c1dff4daaed53a5957369d81a0dfa3806a7afc93f2ee2fd3f3ba3ede86e802139f6d8a81a4dfc75f9bf0e89ab7d607472d9c84488300d70139a1
6
+ metadata.gz: cc51733b2f9e672c36766706ab55cc52cc14e530b1de6d46b9bfa9bc323ff9bd7f4d268f727ea4f2e833e28ec7e47f53b25c3218b2a434ff05f0e86a8b122451
7
+ data.tar.gz: 0ce870a40d24b737bf8a4e45d0fbec3763bbd0ec6e65e76ebc9a65bfdd44aa6cbaf677f84e61ad7060b8dd209a94ef3ba69fae20a1a63a1cc19c9348b099d67a
data/README.md CHANGED
@@ -100,7 +100,7 @@ client = InternetData::Client.new(api_key: ENV['INTERNETDATA_API_KEY'], timeout:
100
100
  catalog = client.database.list(timeout: 5)
101
101
  ```
102
102
 
103
- `timeout` is in seconds and bounds each attempt, body included, so a call that is retried can take longer in total. It defaults to 30 seconds. The client's value is the default: from 2.1.0, `list`, `metadata`, `checksums`, `downloads` and `download_url` each take `timeout:` for that call alone.
103
+ `timeout` is in seconds and bounds each attempt, body included, so a call that is retried can take longer in total. It defaults to 30 seconds, and 0 means no bound. The client's value is the default: from 2.1.0, `list`, `metadata`, `checksums`, `downloads` and `download_url` each take `timeout:` for that call alone. A negative value, anything that isn't a number, and anything past 2147483.647 seconds (the longest curl holds) raise `ArgumentError` where you pass them, before any request.
104
104
 
105
105
  `download` and `download_bytes` take no `timeout:` and raise `ArgumentError` if handed one, rather than accepting it and quietly doing nothing: a transfer runs to gigabytes and minutes, so any bound that suits a JSON call would abandon a healthy download. They bound only their connection with the client's value. `download_url` does take one, because minting the link is an ordinary API request - it bounds that request, not whatever you do with the link afterwards.
106
106
 
@@ -118,7 +118,7 @@ end
118
118
 
119
119
  `kind` is one of `:bad_request`, `:unauthorized`, `:forbidden`, `:rate_limited`, `:quota_exceeded`, `:server_error` or `:network`. `rc` is the API's own result code, such as `NOT_LICENSED` or `LICENSE_EXPIRED`, which is usually the specific thing you want to read.
120
120
 
121
- Note that `:rate_limited` and `:quota_exceeded` both arrive as HTTP 429 and are not the same thing. A rate limit is the API facing a burst, so retrying later works; a spent quota needs your allowance raised or the window to roll over. The library retries the first for you and never the second.
121
+ Note that `:rate_limited` and `:quota_exceeded` both arrive as HTTP 429 and are not the same thing. A rate limit is the API facing a burst, so retrying later works; a spent quota needs your allowance raised or the window to roll over. The library retries the first for you and never the second. It waits the `Retry-After` the API sent, or its own backoff from 250 ms when that's past about 24.8 days.
122
122
 
123
123
  ### Sign in with OAuth (device flow)
124
124
 
@@ -21,9 +21,13 @@ module InternetData
21
21
 
22
22
  # @param api_key [String, nil] a key carrying the `db.download` scope. Omit
23
23
  # it and no credential is sent; every database published today answers 401.
24
- # @param retries [Integer] extra attempts for a transient failure.
25
- # @param timeout [Numeric] seconds allowed for one API call. It also bounds the
26
- # CONNECT phase of a transfer, which is otherwise unlimited.
24
+ # @param retries [Integer] extra attempts for a transient failure. Each waits
25
+ # the server's `Retry-After` when it sent one of at most about 24.8 days,
26
+ # and otherwise a backoff of 250 ms that doubles per retry.
27
+ # @param timeout [Numeric] seconds allowed for one API call, 0 for no bound. It
28
+ # also bounds the CONNECT phase of a transfer, which is otherwise unlimited.
29
+ # @raise [ArgumentError] for a `timeout` that is negative, not a finite number,
30
+ # or past 2147483.647 seconds (2**31 - 1 ms), the longest curl holds.
27
31
  # @param transport [Transport, nil] override the HTTP layer, mostly for tests.
28
32
  def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, retries: DEFAULT_RETRIES,
29
33
  timeout: DEFAULT_TIMEOUT, transport: nil)
@@ -7,7 +7,8 @@ module InternetData
7
7
  # the generated {DatabaseV2Api} underneath, which speaks the wire.
8
8
  #
9
9
  # Every JSON call here takes `timeout:`, in seconds, bounding each ATTEMPT of
10
- # that call alone and overriding the bound the client was built with. The two
10
+ # that call alone and overriding the bound the client was built with; one the
11
+ # client would refuse raises ArgumentError before any request. The two
11
12
  # transfers take none, and are refused it rather than ignoring it: a database
12
13
  # runs to gigabytes and minutes, so a bound that suits a JSON call would
13
14
  # abandon a healthy download.
@@ -17,6 +17,8 @@ module InternetData
17
17
  TOKEN_PATH = '/oauth/token'
18
18
  REVOKE_PATH = '/oauth/revoke'
19
19
  DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code'
20
+ # The longest single `sleep` the poll asks Ruby for, in seconds.
21
+ LONGEST_SLEEP = 2**31 - 1
20
22
 
21
23
  # The members a 2xx must carry for its type to mean anything.
22
24
  REQUIRED = {
@@ -29,7 +31,7 @@ module InternetData
29
31
  @transport = transport
30
32
  @retries = retries
31
33
  # The poll's wait and its monotonic clock, which a test replaces together.
32
- @wait = ->(seconds) { sleep(seconds) }
34
+ @wait = ->(seconds) { sleep_in_parts(seconds) }
33
35
  @now = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
34
36
  end
35
37
 
@@ -96,7 +98,8 @@ module InternetData
96
98
  # Wait for the person to approve a device sign-in, and return its tokens.
97
99
  #
98
100
  # Waits `device.interval` seconds before EVERY exchange, the first included,
99
- # and five seconds longer for good each time the server answers `slow_down`.
101
+ # and five seconds longer for good each time the server answers `slow_down`,
102
+ # but never past `device.expires_in`: a wait that would end later ends then.
100
103
  # Raises {OauthAccessDeniedError} when the person refuses and
101
104
  # {OauthExpiredTokenError} when the code expires, including locally, with no
102
105
  # status, once `device.expires_in` seconds have passed since this call. Any
@@ -109,10 +112,14 @@ module InternetData
109
112
  # @param timeout [Numeric, nil] bounds each exchange, never the whole wait.
110
113
  # @return [TokenResponse]
111
114
  def poll_device_token(client_id, device, timeout: nil)
115
+ Transport.checked_timeout(timeout) unless timeout.nil?
112
116
  interval = device.interval >= 1 ? device.interval : 5
113
117
  deadline = @now.call + device.expires_in
114
118
  loop do
115
- @wait.call(interval)
119
+ # A wait that would end past the deadline waits only the time left, never
120
+ # a negative remainder, and the local expiry below follows with no request.
121
+ left = deadline - @now.call
122
+ @wait.call(interval < left ? interval : [left, 0].max)
116
123
  raise OauthExpiredTokenError, 'expired_token' if @now.call >= deadline
117
124
 
118
125
  begin
@@ -129,6 +136,17 @@ module InternetData
129
136
 
130
137
  private
131
138
 
139
+ # `sleep` raises RangeError for a Float from about 9.2e18 s and an Integer
140
+ # past 2**63 - 1, and a server's `expires_in` can leave a wait longer than
141
+ # either, so a long one is slept in parts rather than ending the poll.
142
+ def sleep_in_parts(seconds)
143
+ while seconds.positive?
144
+ part = [seconds, LONGEST_SLEEP].min
145
+ sleep(part)
146
+ seconds -= part
147
+ end
148
+ end
149
+
132
150
  def exchange(form, timeout)
133
151
  decode(TokenResponse, @transport.oauth_request(:POST, TOKEN_PATH, form: form, timeout: timeout).run)
134
152
  end
@@ -9,6 +9,10 @@ module InternetData
9
9
  module Retries
10
10
  BACKOFF_SECONDS = 0.25
11
11
 
12
+ # The longest `Retry-After` honored, in seconds: 2**31 - 1 ms, about 24.8
13
+ # days, the same bound as the .NET, erlang and perl SDKs'.
14
+ LONGEST_WAIT = 2_147_483.647
15
+
12
16
  module_function
13
17
 
14
18
  # `retry_if`, when given, is asked after each retryable failure, and a false
@@ -26,10 +30,16 @@ module InternetData
26
30
  end
27
31
  end
28
32
 
29
- # A server-supplied delay always wins, including a `Retry-After: 0`, which
30
- # is the server saying "immediately" rather than saying nothing.
33
+ # A server-supplied delay wins, including a `Retry-After: 0`, which is the
34
+ # server saying "immediately" rather than saying nothing. One past
35
+ # LONGEST_WAIT is waited out on the backoff instead, still rate_limited:
36
+ # honored, `2147483648` held the call for 68 years, and `sleep` raised a raw
37
+ # RangeError for `9223372036854775807`.
31
38
  def delay_for(error, attempt)
32
- error.retry_after_seconds || BACKOFF_SECONDS * (2**(attempt - 1))
39
+ asked = error.retry_after_seconds
40
+ return asked if asked && asked <= LONGEST_WAIT
41
+
42
+ BACKOFF_SECONDS * (2**(attempt - 1))
33
43
  end
34
44
  end
35
45
  end
@@ -7,6 +7,23 @@ module InternetData
7
7
  # The generated wire client, with the three things it gets wrong for this API
8
8
  # corrected in one place.
9
9
  class Transport < ApiClient
10
+ # The longest `timeout` curl holds, in seconds: 2**31 - 1 ms, about 24.8 days.
11
+ # libcurl refuses a longer one, and any negative, and Ethon ignores the
12
+ # refusal, so the call would run with no bound at all.
13
+ LONGEST_TIMEOUT = 2_147_483.647
14
+
15
+ # `timeout`, once it is a bound curl can hold: seconds, 0 for none. Refused
16
+ # where it is set, because nothing downstream refuses it: a negative ran with
17
+ # no bound, and NaN, Infinity, a number past curl's ceiling or a string failed
18
+ # every call with an error from Ruby, FFI or Ethon that names none of this.
19
+ # NaN fails both comparisons, and an infinity one of them.
20
+ def self.checked_timeout(timeout)
21
+ return timeout if timeout.is_a?(Numeric) && timeout.real? && timeout >= 0 && timeout <= LONGEST_TIMEOUT
22
+
23
+ raise ArgumentError,
24
+ "timeout must be a number of seconds from 0 (no bound) to #{LONGEST_TIMEOUT}, not #{timeout.inspect}"
25
+ end
26
+
10
27
  # The generated Configuration applies a security scheme whatever it holds, so
11
28
  # a keyless client would send `Authorization: Bearer ` - an empty credential,
12
29
  # which the API answers 401 to rather than treating as no credential at all.
@@ -19,7 +36,7 @@ module InternetData
19
36
  self.host = uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}"
20
37
  self.base_path = uri.path
21
38
  self.access_token = api_key
22
- self.timeout = timeout
39
+ self.timeout = timeout.nil? ? nil : Transport.checked_timeout(timeout)
23
40
  end
24
41
 
25
42
  # ONLY the bearer scheme, never `super`.
@@ -59,7 +76,7 @@ module InternetData
59
76
  def build_request(http_method, path, opts = {})
60
77
  request = super
61
78
  request.options[:followlocation] = false
62
- request.options[:timeout] = opts[:timeout] unless opts[:timeout].nil?
79
+ request.options[:timeout] = Transport.checked_timeout(opts[:timeout]) unless opts[:timeout].nil?
63
80
  request
64
81
  end
65
82
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InternetData
4
- VERSION = '2.3.1'
4
+ VERSION = '2.3.2'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: internetdata
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mslm Dev