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 +4 -4
- data/README.md +2 -2
- data/lib/internetdata/client.rb +7 -3
- data/lib/internetdata/database_api.rb +2 -1
- data/lib/internetdata/oauth_api.rb +21 -3
- data/lib/internetdata/retries.rb +13 -3
- data/lib/internetdata/transport.rb +19 -2
- data/lib/internetdata/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75d59c125ee280d0f8d9fe9522fa6003ac9a1e6fc764839f93f2907f3ee7d13f
|
|
4
|
+
data.tar.gz: 9ee66403a693539439df78eb8cb65306f84093fd6480a92361650b7cd6bc987f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
data/lib/internetdata/client.rb
CHANGED
|
@@ -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
|
-
#
|
|
26
|
-
#
|
|
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
|
|
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) {
|
|
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
|
-
|
|
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
|
data/lib/internetdata/retries.rb
CHANGED
|
@@ -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
|
|
30
|
-
#
|
|
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
|
|
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
|
|
data/lib/internetdata/version.rb
CHANGED