internetdata 2.0.0 → 2.1.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 +4 -4
- data/README.md +13 -1
- data/lib/internetdata/client.rb +1 -1
- data/lib/internetdata/database_api.rb +48 -22
- data/lib/internetdata/retries.rb +4 -2
- data/lib/internetdata/transport.rb +6 -0
- 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: 4c4799411a10284db486537f26bec70503a77692f56751121b34ec4bf89b2b22
|
|
4
|
+
data.tar.gz: db8745ef831d08fbfec78824c184acc194f1eb4bc62ba2e741f530ffff76fd40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69292ca30d27411851f776f88a067176a14ef39331f5344dbd30ae345ca369357456b7bf353323f5c7e9f323f695600da02b9723e6a1e663806d36972f8e67a8
|
|
7
|
+
data.tar.gz: 9d5aa3458e1199c8ed121ecca2a1418dcab23c0725dac8a584f51ffa8ff13b0c1d4f7ac7b4d7a13408e5b928b25c3966a00d1f235ad768d113c3ef5513438762
|
data/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Requires Ruby 3.1 or newer.
|
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
26
|
-
Every database published today needs an API key carrying the `db.download` scope. Access is granted by contract, one family at a time, so there is no self-serve tier: write to [dev@internetdata.io](mailto:dev@internetdata.io) to be licensed and issued a key. `api_key:` is nevertheless optional - a client built without one sends no `Authorization` header at all, ready for a database served without a
|
|
26
|
+
Every database published today needs an API key carrying the `db.download` scope. Access is granted by contract, one family at a time, so there is no self-serve tier: write to [dev@internetdata.io](mailto:dev@internetdata.io) to be licensed and issued a key. `api_key:` is nevertheless optional - a client built without one sends no `Authorization` header at all, ready for a database served without a license.
|
|
27
27
|
|
|
28
28
|
```ruby
|
|
29
29
|
require 'internetdata'
|
|
@@ -92,6 +92,18 @@ client.database.downloads(limit: 20).each do |attempt|
|
|
|
92
92
|
end
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### Timeouts
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
client = InternetData::Client.new(api_key: ENV['INTERNETDATA_API_KEY'], timeout: 10)
|
|
99
|
+
|
|
100
|
+
catalog = client.database.list(timeout: 5)
|
|
101
|
+
```
|
|
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.
|
|
104
|
+
|
|
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
|
+
|
|
95
107
|
### Errors
|
|
96
108
|
|
|
97
109
|
Failures raise an `InternetData::Error` carrying a `kind` and a `retryable?` flag:
|
data/lib/internetdata/client.rb
CHANGED
|
@@ -12,7 +12,7 @@ module InternetData
|
|
|
12
12
|
# Every database published today needs an API key carrying the `db.download`
|
|
13
13
|
# scope, granted by contract one family at a time. The key is still OPTIONAL:
|
|
14
14
|
# a client built without one sends no `Authorization` header at all, which is
|
|
15
|
-
# what a database served without a
|
|
15
|
+
# what a database served without a license would need.
|
|
16
16
|
class Client
|
|
17
17
|
# The licensed database downloads, and everything about them.
|
|
18
18
|
attr_reader :database
|
|
@@ -5,6 +5,12 @@ module InternetData
|
|
|
5
5
|
#
|
|
6
6
|
# Not to be confused with {Database}, which is one entry in a {#list}, nor with
|
|
7
7
|
# the generated {DatabaseV2Api} underneath, which speaks the wire.
|
|
8
|
+
#
|
|
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
|
|
11
|
+
# transfers take none, and are refused it rather than ignoring it: a database
|
|
12
|
+
# runs to gigabytes and minutes, so a bound that suits a JSON call would
|
|
13
|
+
# abandon a healthy download.
|
|
8
14
|
class DatabaseApi
|
|
9
15
|
def initialize(transport, retries:)
|
|
10
16
|
@transport = transport
|
|
@@ -25,8 +31,10 @@ module InternetData
|
|
|
25
31
|
# server decides that per key. So the catalog is not the same for every key,
|
|
26
32
|
# a listing fetched with one key says nothing about another, and there is no
|
|
27
33
|
# other source to reconstruct it from.
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
#
|
|
35
|
+
# @param timeout [Numeric, nil] seconds this attempt may take, for THIS call only.
|
|
36
|
+
def list(timeout: nil)
|
|
37
|
+
call { @api.list_databases(timeout: timeout).databases }
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
# What is inside one database: schema, sample rows, row count and sizes.
|
|
@@ -34,8 +42,10 @@ module InternetData
|
|
|
34
42
|
# `updated` and `entries` answer whether today's build is worth fetching, and
|
|
35
43
|
# `size` is bytes per format, which is what a transfer should be budgeted
|
|
36
44
|
# against before it starts.
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
#
|
|
46
|
+
# @param timeout [Numeric, nil] seconds this attempt may take, for THIS call only.
|
|
47
|
+
def metadata(id, timeout: nil)
|
|
48
|
+
call { @api.database_metadata_v2(id, timeout: timeout) }
|
|
39
49
|
end
|
|
40
50
|
|
|
41
51
|
# The digests for one published file.
|
|
@@ -43,15 +53,19 @@ module InternetData
|
|
|
43
53
|
# Returns the whole set rather than one algorithm: which digests a database
|
|
44
54
|
# publishes is the API's choice, not ours, and the response nests them one
|
|
45
55
|
# level down under `checksums`.
|
|
46
|
-
|
|
56
|
+
#
|
|
57
|
+
# @param timeout [Numeric, nil] seconds this attempt may take, for THIS call only.
|
|
58
|
+
def checksums(id, format, timeout: nil)
|
|
47
59
|
check_format!(format)
|
|
48
|
-
call { @api.database_checksum_v2(id, format).checksums }
|
|
60
|
+
call { @api.database_checksum_v2(id, format, timeout: timeout).checksums }
|
|
49
61
|
end
|
|
50
62
|
|
|
51
63
|
# Your organization's recent download attempts, newest first, refusals
|
|
52
64
|
# included.
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
#
|
|
66
|
+
# @param timeout [Numeric, nil] seconds this attempt may take, for THIS call only.
|
|
67
|
+
def downloads(limit: nil, timeout: nil)
|
|
68
|
+
call { @api.list_downloads(limit: limit, timeout: timeout).downloads }
|
|
55
69
|
end
|
|
56
70
|
|
|
57
71
|
# The time-limited URL for one database file.
|
|
@@ -61,9 +75,13 @@ module InternetData
|
|
|
61
75
|
# caller decides how to transfer a file that routinely runs to gigabytes; the
|
|
62
76
|
# link authorizes the START of a transfer, so one already running is not
|
|
63
77
|
# interrupted when it lapses.
|
|
64
|
-
|
|
78
|
+
#
|
|
79
|
+
# @param timeout [Numeric, nil] seconds this attempt may take, for THIS call
|
|
80
|
+
# only. It bounds the request that MINTS the link, which is an ordinary
|
|
81
|
+
# JSON call, and says nothing about the transfer you then run with it.
|
|
82
|
+
def download_url(id, format, timeout: nil)
|
|
65
83
|
check_format!(format)
|
|
66
|
-
call { redirect_location(id, format) }
|
|
84
|
+
call { redirect_location(id, format, timeout) }
|
|
67
85
|
end
|
|
68
86
|
|
|
69
87
|
# Download one database file to `path`, and return the bytes written.
|
|
@@ -77,11 +95,7 @@ module InternetData
|
|
|
77
95
|
partial = "#{path}.part"
|
|
78
96
|
begin
|
|
79
97
|
url = download_url(id, format)
|
|
80
|
-
written =
|
|
81
|
-
# Reopened per attempt, so a retry restarts the file rather than
|
|
82
|
-
# appending a second copy of the body to a half-written one.
|
|
83
|
-
File.open(partial, 'wb') { |file| stream(url) { |chunk| file.write(chunk) } }
|
|
84
|
-
end
|
|
98
|
+
written = File.open(partial, 'wb') { |file| transfer(url) { |chunk| file.write(chunk) } }
|
|
85
99
|
File.rename(partial, path)
|
|
86
100
|
rescue StandardError
|
|
87
101
|
File.delete(partial) if File.exist?(partial)
|
|
@@ -99,15 +113,27 @@ module InternetData
|
|
|
99
113
|
# not measured with {#metadata}.
|
|
100
114
|
def download_bytes(id, format)
|
|
101
115
|
url = download_url(id, format)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
bytes
|
|
106
|
-
end
|
|
116
|
+
bytes = String.new(encoding: Encoding::BINARY)
|
|
117
|
+
transfer(url) { |chunk| bytes << chunk }
|
|
118
|
+
bytes
|
|
107
119
|
end
|
|
108
120
|
|
|
109
121
|
private
|
|
110
122
|
|
|
123
|
+
# The transfer of a presigned link, retried only while nothing has reached the
|
|
124
|
+
# block: object storage failing before the body is as transient as any
|
|
125
|
+
# outage, while a body that dies part way is not fetched again, because the
|
|
126
|
+
# bytes already handed over cannot be taken back.
|
|
127
|
+
def transfer(url, &sink)
|
|
128
|
+
delivered = false
|
|
129
|
+
Retries.with_retries(@retries, retry_if: -> { !delivered }) do
|
|
130
|
+
stream(url) do |chunk|
|
|
131
|
+
delivered = true
|
|
132
|
+
sink.call(chunk)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
111
137
|
# Runs one transfer of a presigned link, handing each chunk to the block, and
|
|
112
138
|
# returns the bytes that reached it.
|
|
113
139
|
#
|
|
@@ -169,8 +195,8 @@ module InternetData
|
|
|
169
195
|
"invalid value for \"format\", must be one of #{DatabaseFormat.all_vars}"
|
|
170
196
|
end
|
|
171
197
|
|
|
172
|
-
def redirect_location(id, format)
|
|
173
|
-
@api.download_database_v2(id, format)
|
|
198
|
+
def redirect_location(id, format, timeout)
|
|
199
|
+
@api.download_database_v2(id, format, timeout: timeout)
|
|
174
200
|
raise Error.new(:server_error, 'expected a redirect to object storage')
|
|
175
201
|
rescue ApiError => e
|
|
176
202
|
raise unless e.code == 302
|
data/lib/internetdata/retries.rb
CHANGED
|
@@ -11,12 +11,14 @@ module InternetData
|
|
|
11
11
|
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# `retry_if`, when given, is asked after each retryable failure, and a false
|
|
15
|
+
# answer ends the attempts there.
|
|
16
|
+
def with_retries(retries, retry_if: nil)
|
|
15
17
|
attempt = 0
|
|
16
18
|
begin
|
|
17
19
|
yield
|
|
18
20
|
rescue Error => e
|
|
19
|
-
raise unless e.retryable? && attempt < retries
|
|
21
|
+
raise unless e.retryable? && attempt < retries && (retry_if.nil? || retry_if.call)
|
|
20
22
|
|
|
21
23
|
attempt += 1
|
|
22
24
|
sleep(delay_for(e, attempt))
|
|
@@ -50,9 +50,15 @@ module InternetData
|
|
|
50
50
|
# is true for every value it can be given, so the download's 302 would be
|
|
51
51
|
# chased and a multi-gigabyte database read into memory. Nothing this API
|
|
52
52
|
# serves is meant to be followed.
|
|
53
|
+
#
|
|
54
|
+
# `opts[:timeout]` is a per-call override of the configured bound, which the
|
|
55
|
+
# generated client would otherwise apply to every request it builds. It
|
|
56
|
+
# reaches here as an ordinary generated `opts` entry, so one seam bounds
|
|
57
|
+
# every call whether the request was built by hand or by the generator.
|
|
53
58
|
def build_request(http_method, path, opts = {})
|
|
54
59
|
request = super
|
|
55
60
|
request.options[:followlocation] = false
|
|
61
|
+
request.options[:timeout] = opts[:timeout] unless opts[:timeout].nil?
|
|
56
62
|
request
|
|
57
63
|
end
|
|
58
64
|
|
data/lib/internetdata/version.rb
CHANGED