internetdata 2.6.0__tar.gz → 2.6.2__tar.gz
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.
- {internetdata-2.6.0 → internetdata-2.6.2}/PKG-INFO +3 -3
- {internetdata-2.6.0 → internetdata-2.6.2}/README.md +2 -2
- {internetdata-2.6.0 → internetdata-2.6.2}/spec/openapi.yaml +4 -5
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/__init__.py +1 -1
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_core.py +28 -7
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/database_v_2/database_metadata_v2.py +8 -9
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/aio.py +8 -2
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/client.py +8 -2
- {internetdata-2.6.0 → internetdata-2.6.2}/testdata/testdata.json +66 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/tests/test_client.py +33 -3
- {internetdata-2.6.0 → internetdata-2.6.2}/tests/test_download.py +26 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/tests/test_oauth.py +86 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/.gitignore +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/LICENSE +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/pyproject.toml +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/scripts/download-spec.sh +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/scripts/generate.sh +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/scripts/publish.sh +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/scripts/v2_subset.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/__init__.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/__init__.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/account/__init__.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/account/account_identity.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/api_keys/__init__.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/api_keys/account_create_apikey.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/api_keys/account_list_apikeys.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/api_keys/account_reveal_apikey.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/api_keys/account_revoke_apikey.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/api_keys/account_rotate_apikey.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/authorization/__init__.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/authorization/oauth_authorize.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/authorization/oauth_device_authorization.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/authorization/oauth_metadata.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/authorization/oauth_revoke.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/authorization/oauth_token.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/database_v_2/__init__.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/database_v_2/database_checksum_v2.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/database_v_2/download_database_v2.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/database_v_2/list_databases.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/database_v_2/list_downloads.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/organization/__init__.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/organization/account_org.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/organization/account_org_members.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/client.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/errors.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/__init__.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_create_apikey_request.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_created_apikey.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_org.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_org_ref.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_org_wrap.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_rc.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_revealed_apikey.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_user.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/apikey_detail.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/apikey_list.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_checksum_v2_response_200.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_format.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_license_type_type_1.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_license_type_type_2_type_1.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_license_type_type_3_type_1.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_metadata.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_metadata_column.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_metadata_sample.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_metadata_sample_additional_property_item.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_metadata_sample_size.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_metadata_schema.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_metadata_size.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_version.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/db_checksums.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/device_authorization.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/device_authorization_request.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/download.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/download_outcome.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/error.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/identity.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/list_databases_response_200.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/list_downloads_response_200.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/oauth_authorize_code_challenge_method.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/oauth_authorize_response_type.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/oauth_error.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/oauth_metadata.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/oauth_revoke_response_200.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/revoke_request.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/standing.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/token_request.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/token_response.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/types.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/errors.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/models.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/py.typed +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/tests/conftest.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/tests/helpers.py +0 -0
- {internetdata-2.6.0 → internetdata-2.6.2}/tests/test_conformance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: internetdata
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.2
|
|
4
4
|
Summary: Official Python client library for the InternetData API. Download and verify licensed IP datasets.
|
|
5
5
|
Project-URL: Homepage, https://internetdata.io
|
|
6
6
|
Project-URL: Documentation, https://docs.internetdata.io
|
|
@@ -225,11 +225,11 @@ Check that `state` came back as you sent it before you exchange `code`, which wo
|
|
|
225
225
|
|
|
226
226
|
## Other Libraries
|
|
227
227
|
|
|
228
|
-
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java,
|
|
228
|
+
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java, and Ruby. See our GitHub at https://github.com/internetdata for more.
|
|
229
229
|
|
|
230
230
|
## About InternetData
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
Geolocation, anonymity, ownership and network databases for IP addresses and AS numbers, licensed as files you download and query yourself.
|
|
233
233
|
|
|
234
234
|
[<img src="https://s3.internetdata.io/internetdata-public/brand/mark.svg" alt="InternetData" height="64"/>](https://internetdata.io/)
|
|
235
235
|
|
|
@@ -191,11 +191,11 @@ Check that `state` came back as you sent it before you exchange `code`, which wo
|
|
|
191
191
|
|
|
192
192
|
## Other Libraries
|
|
193
193
|
|
|
194
|
-
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java,
|
|
194
|
+
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java, and Ruby. See our GitHub at https://github.com/internetdata for more.
|
|
195
195
|
|
|
196
196
|
## About InternetData
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
Geolocation, anonymity, ownership and network databases for IP addresses and AS numbers, licensed as files you download and query yourself.
|
|
199
199
|
|
|
200
200
|
[<img src="https://s3.internetdata.io/internetdata-public/brand/mark.svg" alt="InternetData" height="64"/>](https://internetdata.io/)
|
|
201
201
|
|
|
@@ -12,7 +12,7 @@ info:
|
|
|
12
12
|
email: support@internetdata.io
|
|
13
13
|
url: https://internetdata.io
|
|
14
14
|
termsOfService: https://internetdata.io/terms
|
|
15
|
-
version: 2026.
|
|
15
|
+
version: 2026.10.03
|
|
16
16
|
servers:
|
|
17
17
|
- url: https://internetdata.io
|
|
18
18
|
tags:
|
|
@@ -126,7 +126,8 @@ paths:
|
|
|
126
126
|
`updated` and `entries` without downloading anything.
|
|
127
127
|
|
|
128
128
|
No `format` parameter - one document describes every format the database
|
|
129
|
-
is built in.
|
|
129
|
+
is built in. Needs no license: it answers for every database `list`
|
|
130
|
+
returns, whatever its `standing`.
|
|
130
131
|
security:
|
|
131
132
|
- ApiKeyQueryAuth: []
|
|
132
133
|
- ApiKeyHeaderAuth: []
|
|
@@ -142,8 +143,6 @@ paths:
|
|
|
142
143
|
$ref: '#/components/schemas/DatabaseMetadata'
|
|
143
144
|
'401':
|
|
144
145
|
$ref: '#/components/responses/V2Unauthorized'
|
|
145
|
-
'403':
|
|
146
|
-
$ref: '#/components/responses/V2NotLicensed'
|
|
147
146
|
'404':
|
|
148
147
|
$ref: '#/components/responses/V2UnknownDatabase'
|
|
149
148
|
'503':
|
|
@@ -1641,7 +1640,7 @@ components:
|
|
|
1641
1640
|
$ref: '#/components/schemas/Error'
|
|
1642
1641
|
V2NotAvailable:
|
|
1643
1642
|
description: |
|
|
1644
|
-
|
|
1643
|
+
The current build has not been published yet.
|
|
1645
1644
|
`rc`: `NOT_AVAILABLE`.
|
|
1646
1645
|
content:
|
|
1647
1646
|
application/json:
|
|
@@ -45,6 +45,20 @@ TRANSFER_CHUNK_BYTES = 1 << 20
|
|
|
45
45
|
|
|
46
46
|
_BACKOFF_BASE = 1.0
|
|
47
47
|
|
|
48
|
+
# The longest wait the sync client's clock can count, about 292 years: past it a lock's
|
|
49
|
+
# wait, `socket.settimeout` and `time.sleep` all raise OverflowError instead.
|
|
50
|
+
WAIT_CEILING = threading.TIMEOUT_MAX
|
|
51
|
+
|
|
52
|
+
# The longest `Retry-After` waited as given, 2^31 - 1 ms, the bound every SDK takes. One
|
|
53
|
+
# past it is still a throttle, waited out on the backoff: as given, 2147484 held a call
|
|
54
|
+
# for 24.8 days, and 9223372036854775807, 1e400 or a year-9999 date raised a raw
|
|
55
|
+
# OverflowError from the sync client's sleep (measured on 2.6.0, 2026-09-30).
|
|
56
|
+
RETRY_AFTER_CEILING = 2147483.647
|
|
57
|
+
|
|
58
|
+
# An `expires_in` past this, about 146 billion years, is a deadline nobody reaches. The
|
|
59
|
+
# cap keeps the poll's deadline a float: past 1.8e308 adding it to the clock raises.
|
|
60
|
+
POLL_DEADLINE_CAP = 2**62
|
|
61
|
+
|
|
48
62
|
T = TypeVar("T")
|
|
49
63
|
|
|
50
64
|
|
|
@@ -124,8 +138,9 @@ def check_timeout(timeout: float | None) -> float | None:
|
|
|
124
138
|
Refused where it is SET, because nothing downstream refuses it: zero, a negative
|
|
125
139
|
number, NaN or a string reached the first call and failed it, and every call after,
|
|
126
140
|
as a retried `network` error after three seconds of backoff, or for a string as a
|
|
127
|
-
`server_error` blaming the API.
|
|
128
|
-
cannot hold it
|
|
141
|
+
`server_error` blaming the API. So is anything past `WAIT_CEILING`, infinity
|
|
142
|
+
included, since the sync client's wait cannot hold it: 1e300 failed every call with
|
|
143
|
+
a raw `OverflowError` (2.6.0, measured 2026-09-30). None is the spelling for no bound.
|
|
129
144
|
"""
|
|
130
145
|
if timeout is None:
|
|
131
146
|
return None
|
|
@@ -134,10 +149,11 @@ def check_timeout(timeout: float | None) -> float | None:
|
|
|
134
149
|
or not isinstance(timeout, int | float)
|
|
135
150
|
or not math.isfinite(timeout)
|
|
136
151
|
or timeout <= 0
|
|
152
|
+
or timeout > WAIT_CEILING
|
|
137
153
|
):
|
|
138
154
|
raise ValueError(
|
|
139
|
-
f"timeout must be a number of seconds greater than 0
|
|
140
|
-
f"not {timeout!r}"
|
|
155
|
+
f"timeout must be a number of seconds greater than 0 and at most "
|
|
156
|
+
f"{WAIT_CEILING:.0f}, or None for no bound, not {timeout!r}"
|
|
141
157
|
)
|
|
142
158
|
return timeout
|
|
143
159
|
|
|
@@ -425,7 +441,11 @@ class Clock:
|
|
|
425
441
|
return time.monotonic()
|
|
426
442
|
|
|
427
443
|
def sleep(self, seconds: float) -> None:
|
|
428
|
-
time.sleep
|
|
444
|
+
# In parts, since `time.sleep` raises past WAIT_CEILING rather than waiting.
|
|
445
|
+
while seconds > 0:
|
|
446
|
+
part = min(seconds, WAIT_CEILING)
|
|
447
|
+
time.sleep(part)
|
|
448
|
+
seconds -= part
|
|
429
449
|
|
|
430
450
|
|
|
431
451
|
class AsyncClock:
|
|
@@ -513,11 +533,12 @@ def retry_delay(err: InternetDataError, attempt: int, retries: int) -> float | N
|
|
|
513
533
|
|
|
514
534
|
A server-supplied `Retry-After` wins over the backoff schedule outright: it is the
|
|
515
535
|
only thing that makes a 429 worth retrying at all, so second-guessing it with a
|
|
516
|
-
shorter wait would just spend the next attempt on the same rejection.
|
|
536
|
+
shorter wait would just spend the next attempt on the same rejection. One past
|
|
537
|
+
`RETRY_AFTER_CEILING` is waited out on the backoff instead.
|
|
517
538
|
"""
|
|
518
539
|
if attempt >= retries or not err.retryable:
|
|
519
540
|
return None
|
|
520
|
-
if err.retry_after_seconds is not None:
|
|
541
|
+
if err.retry_after_seconds is not None and err.retry_after_seconds <= RETRY_AFTER_CEILING:
|
|
521
542
|
return err.retry_after_seconds
|
|
522
543
|
return _BACKOFF_BASE * (2.0**attempt)
|
|
523
544
|
|
|
@@ -43,11 +43,6 @@ def _parse_response(
|
|
|
43
43
|
|
|
44
44
|
return response_401
|
|
45
45
|
|
|
46
|
-
if response.status_code == 403:
|
|
47
|
-
response_403 = Error.from_dict(response.json())
|
|
48
|
-
|
|
49
|
-
return response_403
|
|
50
|
-
|
|
51
46
|
if response.status_code == 404:
|
|
52
47
|
response_404 = Error.from_dict(response.json())
|
|
53
48
|
|
|
@@ -86,7 +81,8 @@ def sync_detailed(
|
|
|
86
81
|
`updated` and `entries` without downloading anything.
|
|
87
82
|
|
|
88
83
|
No `format` parameter - one document describes every format the database
|
|
89
|
-
is built in.
|
|
84
|
+
is built in. Needs no license: it answers for every database `list`
|
|
85
|
+
returns, whatever its `standing`.
|
|
90
86
|
|
|
91
87
|
Args:
|
|
92
88
|
id (str): Example: vpn_ip_v1.
|
|
@@ -121,7 +117,8 @@ def sync(
|
|
|
121
117
|
`updated` and `entries` without downloading anything.
|
|
122
118
|
|
|
123
119
|
No `format` parameter - one document describes every format the database
|
|
124
|
-
is built in.
|
|
120
|
+
is built in. Needs no license: it answers for every database `list`
|
|
121
|
+
returns, whatever its `standing`.
|
|
125
122
|
|
|
126
123
|
Args:
|
|
127
124
|
id (str): Example: vpn_ip_v1.
|
|
@@ -151,7 +148,8 @@ async def asyncio_detailed(
|
|
|
151
148
|
`updated` and `entries` without downloading anything.
|
|
152
149
|
|
|
153
150
|
No `format` parameter - one document describes every format the database
|
|
154
|
-
is built in.
|
|
151
|
+
is built in. Needs no license: it answers for every database `list`
|
|
152
|
+
returns, whatever its `standing`.
|
|
155
153
|
|
|
156
154
|
Args:
|
|
157
155
|
id (str): Example: vpn_ip_v1.
|
|
@@ -184,7 +182,8 @@ async def asyncio(
|
|
|
184
182
|
`updated` and `entries` without downloading anything.
|
|
185
183
|
|
|
186
184
|
No `format` parameter - one document describes every format the database
|
|
187
|
-
is built in.
|
|
185
|
+
is built in. Needs no license: it answers for every database `list`
|
|
186
|
+
returns, whatever its `standing`.
|
|
188
187
|
|
|
189
188
|
Args:
|
|
190
189
|
id (str): Example: vpn_ip_v1.
|
|
@@ -26,6 +26,7 @@ from ._core import (
|
|
|
26
26
|
OAUTH_METADATA_PATH,
|
|
27
27
|
OAUTH_REVOKE_PATH,
|
|
28
28
|
OAUTH_TOKEN_PATH,
|
|
29
|
+
POLL_DEADLINE_CAP,
|
|
29
30
|
TRANSFER_CHUNK_BYTES,
|
|
30
31
|
AsyncClock,
|
|
31
32
|
as_error,
|
|
@@ -451,9 +452,14 @@ class AsyncOauthApi:
|
|
|
451
452
|
# Refused before the first wait, not after it.
|
|
452
453
|
check_timeout(timeout)
|
|
453
454
|
interval = device.interval if device.interval >= 1 else 5
|
|
454
|
-
deadline
|
|
455
|
+
# Capped where a deadline stops meaning anything, since the clock is a float and
|
|
456
|
+
# `expires_in` is the server's integer, of any size.
|
|
457
|
+
deadline = self._clock.now() + min(device.expires_in, POLL_DEADLINE_CAP)
|
|
455
458
|
while True:
|
|
456
|
-
|
|
459
|
+
# No wait runs past the deadline: an interval ending after it waits only the
|
|
460
|
+
# time left, and the expiry follows with nothing sent. In full, an interval of
|
|
461
|
+
# 2147483647 held a poll with 2 s left for 68 years (2.6.0).
|
|
462
|
+
await self._clock.sleep(min(interval, max(deadline - self._clock.now(), 0)))
|
|
457
463
|
if self._clock.now() >= deadline:
|
|
458
464
|
raise OauthExpiredTokenError()
|
|
459
465
|
try:
|
|
@@ -21,6 +21,7 @@ from ._core import (
|
|
|
21
21
|
OAUTH_METADATA_PATH,
|
|
22
22
|
OAUTH_REVOKE_PATH,
|
|
23
23
|
OAUTH_TOKEN_PATH,
|
|
24
|
+
POLL_DEADLINE_CAP,
|
|
24
25
|
TRANSFER_CHUNK_BYTES,
|
|
25
26
|
Clock,
|
|
26
27
|
as_error,
|
|
@@ -497,9 +498,14 @@ class OauthApi:
|
|
|
497
498
|
# Refused before the first wait, not after it.
|
|
498
499
|
check_timeout(timeout)
|
|
499
500
|
interval = device.interval if device.interval >= 1 else 5
|
|
500
|
-
deadline
|
|
501
|
+
# Capped where a deadline stops meaning anything, since the clock is a float and
|
|
502
|
+
# `expires_in` is the server's integer, of any size.
|
|
503
|
+
deadline = self._clock.now() + min(device.expires_in, POLL_DEADLINE_CAP)
|
|
501
504
|
while True:
|
|
502
|
-
|
|
505
|
+
# No wait runs past the deadline: an interval ending after it waits only the
|
|
506
|
+
# time left, and the expiry follows with nothing sent. In full, an interval of
|
|
507
|
+
# 2147483647 held a poll with 2 s left for 68 years (2.6.0).
|
|
508
|
+
self._clock.sleep(min(interval, max(deadline - self._clock.now(), 0)))
|
|
503
509
|
if self._clock.now() >= deadline:
|
|
504
510
|
raise OauthExpiredTokenError()
|
|
505
511
|
try:
|
|
@@ -1215,6 +1215,72 @@
|
|
|
1215
1215
|
5
|
|
1216
1216
|
]
|
|
1217
1217
|
}
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
"name": "interval-past-the-deadline-sleeps-only-to-it",
|
|
1221
|
+
"clientId": "internetdata-cli",
|
|
1222
|
+
"device": {
|
|
1223
|
+
"device_code": "mo_dc_poll",
|
|
1224
|
+
"user_code": "BCDF-GHJK",
|
|
1225
|
+
"verification_uri": "https://app.internetdata.io/device",
|
|
1226
|
+
"verification_uri_complete": "https://app.internetdata.io/device?user_code=BCDF-GHJK",
|
|
1227
|
+
"expires_in": 2,
|
|
1228
|
+
"interval": 2147483647
|
|
1229
|
+
},
|
|
1230
|
+
"why": "A sleep that would end past the local deadline ends at it, and the local expiry follows with no request sent: the outcome the full sleep reaches anyway, without asking the runtime for a sleep it may not hold (.NET's Task.Delay throws past ~49.7 days). 2147483647 is the largest interval every SDK's type holds.",
|
|
1231
|
+
"responses": [
|
|
1232
|
+
{
|
|
1233
|
+
"status": 400,
|
|
1234
|
+
"body": {
|
|
1235
|
+
"error": "authorization_pending"
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
],
|
|
1239
|
+
"expect": {
|
|
1240
|
+
"outcome": "expiredToken",
|
|
1241
|
+
"requests": 0,
|
|
1242
|
+
"waits": [
|
|
1243
|
+
2
|
|
1244
|
+
],
|
|
1245
|
+
"status": null
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
"name": "slow-down-past-the-deadline-sleeps-only-to-it",
|
|
1250
|
+
"clientId": "internetdata-cli",
|
|
1251
|
+
"device": {
|
|
1252
|
+
"device_code": "mo_dc_poll",
|
|
1253
|
+
"user_code": "BCDF-GHJK",
|
|
1254
|
+
"verification_uri": "https://app.internetdata.io/device",
|
|
1255
|
+
"verification_uri_complete": "https://app.internetdata.io/device?user_code=BCDF-GHJK",
|
|
1256
|
+
"expires_in": 8,
|
|
1257
|
+
"interval": 1
|
|
1258
|
+
},
|
|
1259
|
+
"why": "The cut applies to every sleep, a widened one included: 1, then 6 after the first slow_down, then only the 1 s left of the 8 rather than 11.",
|
|
1260
|
+
"responses": [
|
|
1261
|
+
{
|
|
1262
|
+
"status": 400,
|
|
1263
|
+
"body": {
|
|
1264
|
+
"error": "slow_down"
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
"status": 400,
|
|
1269
|
+
"body": {
|
|
1270
|
+
"error": "slow_down"
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
],
|
|
1274
|
+
"expect": {
|
|
1275
|
+
"outcome": "expiredToken",
|
|
1276
|
+
"requests": 2,
|
|
1277
|
+
"waits": [
|
|
1278
|
+
1,
|
|
1279
|
+
6,
|
|
1280
|
+
1
|
|
1281
|
+
],
|
|
1282
|
+
"status": null
|
|
1283
|
+
}
|
|
1218
1284
|
}
|
|
1219
1285
|
]
|
|
1220
1286
|
},
|
|
@@ -357,6 +357,34 @@ def test_a_rate_limit_is_retried_and_honors_retry_after(make_client: ClientFacto
|
|
|
357
357
|
assert len(stub.requests) == 3
|
|
358
358
|
|
|
359
359
|
|
|
360
|
+
@pytest.mark.parametrize(
|
|
361
|
+
"retry_after", ["2147484", "9223372036854775807", "1e400", "Fri, 31 Dec 9999 23:59:59 GMT"]
|
|
362
|
+
)
|
|
363
|
+
def test_a_retry_after_past_the_ceiling_waits_the_backoff_and_stays_a_throttle(
|
|
364
|
+
make_client: ClientFactory, monkeypatch: pytest.MonkeyPatch, retry_after: str
|
|
365
|
+
) -> None:
|
|
366
|
+
"""As given, 2147484 held the call for 24.8 days, and the rest raised a raw
|
|
367
|
+
OverflowError from the sync client's sleep."""
|
|
368
|
+
monkeypatch.setattr(_core, "_BACKOFF_BASE", 0.0)
|
|
369
|
+
headers = {"Retry-After": retry_after}
|
|
370
|
+
stub = Stub({LIST_PATH: {"status": 429, "body": {"rc": "RATE_LIMITED"}, "headers": headers}})
|
|
371
|
+
client = make_client(transport=stub.transport, retries=1)
|
|
372
|
+
|
|
373
|
+
outcome = settle(client.database.list, within=5.0)
|
|
374
|
+
|
|
375
|
+
assert isinstance(outcome, InternetDataError), f"settled with {outcome!r}"
|
|
376
|
+
assert outcome.kind == "rate_limited"
|
|
377
|
+
assert len(stub.requests) == 2
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_a_retry_after_is_waited_as_given_up_to_the_ceiling() -> None:
|
|
381
|
+
def throttled(seconds: float) -> InternetDataError:
|
|
382
|
+
return InternetDataError("rate_limited", "RATE_LIMITED", 429, seconds)
|
|
383
|
+
|
|
384
|
+
assert _core.retry_delay(throttled(2147483.647), 0, 1) == 2147483.647
|
|
385
|
+
assert _core.retry_delay(throttled(2147483.648), 0, 1) == _core._BACKOFF_BASE
|
|
386
|
+
|
|
387
|
+
|
|
360
388
|
def test_a_transport_failure_surfaces_as_a_network_error() -> None:
|
|
361
389
|
def refuse(request: httpx.Request) -> httpx.Response:
|
|
362
390
|
raise httpx.ConnectError("connection refused", request=request)
|
|
@@ -441,7 +469,7 @@ def test_a_per_call_timeout_bounds_a_trickling_body_and_leaves_the_clients_own_a
|
|
|
441
469
|
_assert_bounded_by(default, CLIENT_TIMEOUT)
|
|
442
470
|
|
|
443
471
|
|
|
444
|
-
@pytest.mark.parametrize("timeout", [0, -1, float("nan"), float("inf"), "30", True])
|
|
472
|
+
@pytest.mark.parametrize("timeout", [0, -1, float("nan"), float("inf"), 1e300, "30", True])
|
|
445
473
|
@pytest.mark.parametrize("call", PER_CALL)
|
|
446
474
|
def test_a_per_call_timeout_no_attempt_can_meet_is_refused_before_any_request(
|
|
447
475
|
make_client: ClientFactory, call: str, timeout: Any
|
|
@@ -494,7 +522,9 @@ def test_the_default_timeout_is_thirty_seconds() -> None:
|
|
|
494
522
|
|
|
495
523
|
|
|
496
524
|
@pytest.mark.parametrize("client", [InternetData, AsyncInternetData])
|
|
497
|
-
@pytest.mark.parametrize(
|
|
525
|
+
@pytest.mark.parametrize(
|
|
526
|
+
"timeout", [0, -1, 0.0, float("nan"), float("inf"), 9223372037, 1e300, "30", True]
|
|
527
|
+
)
|
|
498
528
|
def test_a_timeout_no_attempt_can_meet_is_refused_when_the_client_is_built(
|
|
499
529
|
client: type[InternetData | AsyncInternetData], timeout: Any
|
|
500
530
|
) -> None:
|
|
@@ -503,7 +533,7 @@ def test_a_timeout_no_attempt_can_meet_is_refused_when_the_client_is_built(
|
|
|
503
533
|
client(API_KEY, timeout=timeout)
|
|
504
534
|
|
|
505
535
|
|
|
506
|
-
@pytest.mark.parametrize("timeout", [None, 0.25, 1, 30])
|
|
536
|
+
@pytest.mark.parametrize("timeout", [None, 0.25, 1, 30, 9223372036])
|
|
507
537
|
def test_a_usable_timeout_builds_a_client(timeout: float | None) -> None:
|
|
508
538
|
InternetData(API_KEY, timeout=timeout).close()
|
|
509
539
|
asyncio.run(AsyncInternetData(API_KEY, timeout=timeout).aclose())
|
|
@@ -326,6 +326,32 @@ def test_a_storage_5xx_before_the_body_is_retried(
|
|
|
326
326
|
assert outcome == SMALL
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
@pytest.mark.parametrize("retry_after", ["2147484", "9223372036854775807"])
|
|
330
|
+
def test_a_storage_retry_after_past_the_ceiling_waits_the_backoff(
|
|
331
|
+
make_client: ClientFactory, monkeypatch: pytest.MonkeyPatch, retry_after: str
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Object storage's 429 is bounded like the API's: as given, one held the transfer for
|
|
334
|
+
24.8 days, and the other raised a raw OverflowError from the sync client's sleep."""
|
|
335
|
+
monkeypatch.setattr(_core, "_BACKOFF_BASE", 0.0)
|
|
336
|
+
requests: list[httpx.Request] = []
|
|
337
|
+
|
|
338
|
+
def handle(request: httpx.Request) -> httpx.Response:
|
|
339
|
+
requests.append(request)
|
|
340
|
+
if request.url.path == DOWNLOAD_PATH:
|
|
341
|
+
return httpx.Response(302, headers={"Location": BLOB})
|
|
342
|
+
return httpx.Response(429, headers={"Retry-After": retry_after})
|
|
343
|
+
|
|
344
|
+
client = make_client(
|
|
345
|
+
api_key=API_KEY, base_url=API, transport=httpx.MockTransport(handle), retries=1
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
outcome = settle(lambda: client.database.download_bytes("bogon_ip_v1", "csvgz"), within=5.0)
|
|
349
|
+
|
|
350
|
+
assert [r.url.path for r in requests].count("/blob") == 2, "storage's 429 and its retry"
|
|
351
|
+
assert isinstance(outcome, InternetDataError), f"settled with {outcome!r}"
|
|
352
|
+
assert outcome.kind == "rate_limited"
|
|
353
|
+
|
|
354
|
+
|
|
329
355
|
@pytest.mark.parametrize("method", ["download", "download_bytes"])
|
|
330
356
|
def test_a_transfer_that_dies_part_way_is_not_fetched_again(
|
|
331
357
|
make_client: ClientFactory, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, method: str
|
|
@@ -305,6 +305,92 @@ def test_poll_device_token_waits_widens_and_ends_as_the_corpus_says(
|
|
|
305
305
|
_assert_outcome(outcome, {**expect, "type": expect["outcome"]}, case["name"])
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
def test_the_poll_sleeps_the_fraction_left_before_its_deadline(make_client: ClientFactory) -> None:
|
|
309
|
+
"""On the real clock, which the corpus's seam replaces. The second wait is the ~0.99 s
|
|
310
|
+
left, so the deadline passes with one request sent; a sleep that dropped the fraction
|
|
311
|
+
would poll again at once, and this stub approves that second poll."""
|
|
312
|
+
bound = LoopBound()
|
|
313
|
+
replies = [
|
|
314
|
+
{"status": 400, "body": {"error": "authorization_pending"}},
|
|
315
|
+
{"status": 200, "body": EVERY_REQUIRED_MEMBER},
|
|
316
|
+
]
|
|
317
|
+
stub = OauthStub(replies, bound, limit=2)
|
|
318
|
+
client = make_client(base_url=BASE_URL, transport=stub.transport)
|
|
319
|
+
device = DeviceAuthorization(
|
|
320
|
+
device_code="mo_dc_x",
|
|
321
|
+
user_code="BCDF-GHJK",
|
|
322
|
+
verification_uri="https://app.example.test/device",
|
|
323
|
+
expires_in=2,
|
|
324
|
+
interval=1,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
outcome = settle(lambda: client.oauth.poll_device_token("internetdata-cli", device), bound)
|
|
328
|
+
|
|
329
|
+
assert isinstance(outcome, OauthExpiredTokenError), f"settled with {outcome!r}"
|
|
330
|
+
assert outcome.status is None, "the local deadline, not the server's"
|
|
331
|
+
assert len(stub.requests) == 1
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_the_real_clock_sleeps_past_what_time_sleep_can_count_in_parts(
|
|
335
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""`time.sleep` raises OverflowError past WAIT_CEILING, and a poll's wait is the server's
|
|
338
|
+
`interval` or `expires_in`, of any size."""
|
|
339
|
+
waits: list[float] = []
|
|
340
|
+
monkeypatch.setattr(time, "sleep", waits.append)
|
|
341
|
+
|
|
342
|
+
_core.Clock().sleep(_core.WAIT_CEILING * 2.5)
|
|
343
|
+
|
|
344
|
+
assert waits == [_core.WAIT_CEILING, _core.WAIT_CEILING, _core.WAIT_CEILING / 2]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_a_poll_whose_request_outlives_the_deadline_waits_nothing_more(
|
|
348
|
+
make_client: ClientFactory,
|
|
349
|
+
) -> None:
|
|
350
|
+
"""The request itself takes the clock 10 s past the deadline, so the time left is
|
|
351
|
+
negative, and `time.sleep` raises ValueError for a negative wait."""
|
|
352
|
+
bound = LoopBound()
|
|
353
|
+
clock = FakeClock(bound)
|
|
354
|
+
stub = OauthStub([{"status": 400, "body": {"error": "authorization_pending"}}], bound, limit=1)
|
|
355
|
+
|
|
356
|
+
def slow(request: httpx.Request) -> httpx.Response:
|
|
357
|
+
clock._elapsed += 10
|
|
358
|
+
return stub._handle(request)
|
|
359
|
+
|
|
360
|
+
client = make_client(base_url=BASE_URL, transport=httpx.MockTransport(slow))
|
|
361
|
+
client.oauth.use_clock(clock)
|
|
362
|
+
device = DeviceAuthorization(**{**_device_fields(), "expires_in": 5, "interval": 1})
|
|
363
|
+
|
|
364
|
+
outcome = settle(lambda: client.oauth.poll_device_token("internetdata-cli", device), bound)
|
|
365
|
+
|
|
366
|
+
assert isinstance(outcome, OauthExpiredTokenError), f"settled with {outcome!r}"
|
|
367
|
+
assert clock.waits == [1, 0]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_an_expires_in_past_any_float_still_polls(make_client: ClientFactory) -> None:
|
|
371
|
+
"""The server's integer, of any size: added to the clock whole, 10**400 raised a raw
|
|
372
|
+
OverflowError before the first wait."""
|
|
373
|
+
bound = LoopBound()
|
|
374
|
+
stub = OauthStub([{"status": 200, "body": EVERY_REQUIRED_MEMBER}], bound, limit=1)
|
|
375
|
+
client = make_client(base_url=BASE_URL, transport=stub.transport)
|
|
376
|
+
clock = FakeClock(bound)
|
|
377
|
+
client.oauth.use_clock(clock)
|
|
378
|
+
device = DeviceAuthorization(**{**_device_fields(), "expires_in": 10**400, "interval": 1})
|
|
379
|
+
|
|
380
|
+
outcome = settle(lambda: client.oauth.poll_device_token("internetdata-cli", device), bound)
|
|
381
|
+
|
|
382
|
+
assert isinstance(outcome, TokenResponse), f"settled with {outcome!r}"
|
|
383
|
+
assert clock.waits == [1]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _device_fields() -> dict[str, Any]:
|
|
387
|
+
return {
|
|
388
|
+
"device_code": "mo_dc_x",
|
|
389
|
+
"user_code": "BCDF-GHJK",
|
|
390
|
+
"verification_uri": "https://app.example.test/device",
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
308
394
|
def test_cancelling_a_poll_during_its_first_wait_ends_it_at_once() -> None:
|
|
309
395
|
bound = LoopBound()
|
|
310
396
|
stub = OauthStub([{"status": 400, "body": {"error": "authorization_pending"}}], bound, limit=1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/account/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/api_keys/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/authorization/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/database_v_2/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/api/organization/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_org_ref.py
RENAMED
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_org_wrap.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/account_user.py
RENAMED
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/apikey_detail.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_format.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/database_version.py
RENAMED
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/db_checksums.py
RENAMED
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/device_authorization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/download_outcome.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/oauth_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/revoke_request.py
RENAMED
|
File without changes
|
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/token_request.py
RENAMED
|
File without changes
|
{internetdata-2.6.0 → internetdata-2.6.2}/src/internetdata/_generated/models/token_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|