internetdata 2.7.1__tar.gz → 2.7.3__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.7.1 → internetdata-2.7.3}/PKG-INFO +1 -1
- {internetdata-2.7.1 → internetdata-2.7.3}/spec/openapi.yaml +12 -15
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/__init__.py +1 -1
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_core.py +78 -73
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/database_v_2/list_databases.py +0 -4
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database.py +6 -7
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/download.py +1 -3
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/aio.py +4 -5
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/client.py +4 -5
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/errors.py +58 -14
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/models.py +40 -24
- {internetdata-2.7.1 → internetdata-2.7.3}/tests/helpers.py +5 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/tests/test_client.py +218 -2
- {internetdata-2.7.1 → internetdata-2.7.3}/tests/test_conformance.py +24 -1
- {internetdata-2.7.1 → internetdata-2.7.3}/.gitignore +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/LICENSE +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/README.md +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/pyproject.toml +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/scripts/download-spec.sh +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/scripts/generate.sh +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/scripts/publish.sh +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/scripts/v2_subset.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/__init__.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/__init__.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/account/__init__.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/account/account_identity.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/api_keys/__init__.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/api_keys/account_create_apikey.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/api_keys/account_list_apikeys.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/api_keys/account_reveal_apikey.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/api_keys/account_revoke_apikey.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/api_keys/account_rotate_apikey.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/authorization/__init__.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/authorization/oauth_authorize.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/authorization/oauth_device_authorization.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/authorization/oauth_metadata.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/authorization/oauth_revoke.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/authorization/oauth_token.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/database_v_2/__init__.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/database_v_2/database_checksum_v2.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/database_v_2/database_metadata_v2.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/database_v_2/download_database_v2.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/database_v_2/list_downloads.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/organization/__init__.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/organization/account_org.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/api/organization/account_org_members.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/client.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/errors.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/__init__.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/account_create_apikey_request.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/account_created_apikey.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/account_org.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/account_org_ref.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/account_org_wrap.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/account_rc.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/account_revealed_apikey.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/account_user.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/apikey_detail.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/apikey_list.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_checksum_v2_response_200.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_format.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_license_type_type_1.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_license_type_type_2_type_1.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_license_type_type_3_type_1.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_metadata.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_metadata_column.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_metadata_sample.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_metadata_sample_additional_property_item.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_metadata_sample_size.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_metadata_schema.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_metadata_size.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/database_version.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/db_checksums.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/device_authorization.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/device_authorization_request.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/download_outcome.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/error.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/identity.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/list_databases_response_200.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/list_downloads_response_200.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/oauth_authorize_code_challenge_method.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/oauth_authorize_response_type.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/oauth_error.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/oauth_metadata.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/oauth_revoke_response_200.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/revoke_request.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/standing.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/token_request.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/models/token_response.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/_generated/types.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/src/internetdata/py.typed +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/testdata/testdata.json +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/tests/conftest.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/tests/test_download.py +0 -0
- {internetdata-2.7.1 → internetdata-2.7.3}/tests/test_oauth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: internetdata
|
|
3
|
-
Version: 2.7.
|
|
3
|
+
Version: 2.7.3
|
|
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
|
|
@@ -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.10.
|
|
15
|
+
version: 2026.10.13
|
|
16
16
|
servers:
|
|
17
17
|
- url: https://internetdata.io
|
|
18
18
|
tags:
|
|
@@ -29,8 +29,9 @@ tags:
|
|
|
29
29
|
|
|
30
30
|
Access is granted by CONTRACT, one dataset family at a time: there is no
|
|
31
31
|
self-service purchase and no subscription behind it. The Open databases
|
|
32
|
-
are
|
|
33
|
-
|
|
32
|
+
are licensed to every organization from the start, a `standard` license
|
|
33
|
+
to each, and are free under CC BY-SA 4.0 besides. `list` marks them
|
|
34
|
+
`open`.
|
|
34
35
|
|
|
35
36
|
A successful download answers `302` with a time-limited URL pointing
|
|
36
37
|
straight at object storage. Follow the redirect (`curl -L`); the link
|
|
@@ -60,7 +61,6 @@ paths:
|
|
|
60
61
|
The whole published catalog, with your organization's license beside
|
|
61
62
|
each entry, so `standing` says whether a database is yours today
|
|
62
63
|
(`licensed`), was (`expired`), or has never been bought (`unlicensed`).
|
|
63
|
-
An `open` database downloads whatever its `standing`.
|
|
64
64
|
security:
|
|
65
65
|
- ApiKeyQueryAuth: []
|
|
66
66
|
- ApiKeyHeaderAuth: []
|
|
@@ -902,8 +902,7 @@ components:
|
|
|
902
902
|
description: |
|
|
903
903
|
Where your license for a database family stands today. `licensed` is a
|
|
904
904
|
live grant, `expired` one whose term has ended, and `unlicensed` a
|
|
905
|
-
database published but never bought.
|
|
906
|
-
downloads whatever this says.
|
|
905
|
+
database published but never bought.
|
|
907
906
|
enum:
|
|
908
907
|
- licensed
|
|
909
908
|
- expired
|
|
@@ -942,11 +941,11 @@ components:
|
|
|
942
941
|
open:
|
|
943
942
|
type: boolean
|
|
944
943
|
description: |
|
|
945
|
-
An Open database:
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
944
|
+
An Open database: free under CC BY-SA 4.0 (credit InternetData,
|
|
945
|
+
https://internetdata.io, and share what you build from it under
|
|
946
|
+
the same license). Every organization holds a `standard` license
|
|
947
|
+
to it from the start, which `standing` and `license_type` report
|
|
948
|
+
like any other.
|
|
950
949
|
license_type:
|
|
951
950
|
type: string
|
|
952
951
|
nullable: true
|
|
@@ -1056,9 +1055,7 @@ components:
|
|
|
1056
1055
|
open:
|
|
1057
1056
|
type: boolean
|
|
1058
1057
|
description: |
|
|
1059
|
-
|
|
1060
|
-
Open database downloaded while your organization held no license
|
|
1061
|
-
in term for it.
|
|
1058
|
+
A download of an Open database.
|
|
1062
1059
|
bytes:
|
|
1063
1060
|
type: integer
|
|
1064
1061
|
format: int64
|
|
@@ -1647,7 +1644,7 @@ components:
|
|
|
1647
1644
|
V2NotLicensed:
|
|
1648
1645
|
description: |
|
|
1649
1646
|
Your organization holds no license for this database, or its term has
|
|
1650
|
-
ended.
|
|
1647
|
+
ended.
|
|
1651
1648
|
`rc`: `NOT_LICENSED`, `LICENSE_EXPIRED`.
|
|
1652
1649
|
content:
|
|
1653
1650
|
application/json:
|
|
@@ -14,7 +14,7 @@ import os
|
|
|
14
14
|
import secrets
|
|
15
15
|
import threading
|
|
16
16
|
import time
|
|
17
|
-
from collections.abc import
|
|
17
|
+
from collections.abc import Callable, Iterator
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from types import ModuleType
|
|
20
20
|
from typing import IO, Any, TypeVar, cast
|
|
@@ -23,7 +23,6 @@ from urllib.parse import quote
|
|
|
23
23
|
import httpx
|
|
24
24
|
|
|
25
25
|
from ._generated.client import AuthenticatedClient, Client
|
|
26
|
-
from ._generated.types import Response
|
|
27
26
|
from .errors import InternetDataError, error_from_response, oauth_error_from
|
|
28
27
|
from .models import Database, Download, Pkce, to_database, to_download
|
|
29
28
|
|
|
@@ -81,10 +80,11 @@ def build_client(
|
|
|
81
80
|
`set_httpx_client()`, which silently bypasses auth: the generated client only adds
|
|
82
81
|
the `Authorization` header when it CONSTRUCTS the httpx client itself.
|
|
83
82
|
"""
|
|
83
|
+
api_key = check_api_key(api_key)
|
|
84
84
|
httpx_args: dict[str, Any] = {}
|
|
85
85
|
if transport is not None:
|
|
86
86
|
httpx_args["transport"] = transport
|
|
87
|
-
if
|
|
87
|
+
if api_key is None:
|
|
88
88
|
return cast(
|
|
89
89
|
AuthenticatedClient,
|
|
90
90
|
Client(base_url=base_url, timeout=httpx.Timeout(timeout), httpx_args=httpx_args),
|
|
@@ -132,6 +132,25 @@ def build_async_transfer_client(
|
|
|
132
132
|
)
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
def check_api_key(api_key: str | None) -> str | None:
|
|
136
|
+
"""`api_key` without the blanks around it, or None when nothing else is left.
|
|
137
|
+
|
|
138
|
+
Refused where it is SET when a control character remains: httpx refuses a header
|
|
139
|
+
holding a CR, LF or NUL, so every call failed as a retried `network` error with nothing
|
|
140
|
+
sent, while a tab or DEL inside the key went out on the wire. Untrimmed, a key of blanks
|
|
141
|
+
was sent as one rather than as no key, and failed every call the same way (2.7.1,
|
|
142
|
+
measured 2026-10-12).
|
|
143
|
+
"""
|
|
144
|
+
if api_key is None:
|
|
145
|
+
return None
|
|
146
|
+
if not isinstance(api_key, str):
|
|
147
|
+
raise TypeError(f"api_key must be a string or None, not {type(api_key).__name__}")
|
|
148
|
+
key = api_key.strip()
|
|
149
|
+
if any(ord(char) < 0x20 or ord(char) == 0x7F for char in key):
|
|
150
|
+
raise ValueError("api_key holds a control character")
|
|
151
|
+
return key or None
|
|
152
|
+
|
|
153
|
+
|
|
135
154
|
def check_timeout(timeout: float | None) -> float | None:
|
|
136
155
|
"""`timeout`, once it is a bound an attempt can meet.
|
|
137
156
|
|
|
@@ -218,67 +237,51 @@ def part_file(destination: str | os.PathLike[str]) -> Iterator[IO[bytes]]:
|
|
|
218
237
|
raise
|
|
219
238
|
|
|
220
239
|
|
|
221
|
-
def send(call: Callable[[], Response[Any]]) -> Response[Any]:
|
|
222
|
-
"""One generated endpoint call.
|
|
223
|
-
|
|
224
|
-
The generated code eagerly decodes the body of every DOCUMENTED status into its model
|
|
225
|
-
before anything here sees the response, and it raises three different ways doing it: a
|
|
226
|
-
503 carrying an intermediary's HTML error page is a `ValueError` out of `json()`, a
|
|
227
|
-
200 missing a required key is a `KeyError`, and a `standing` this pinned spec predates
|
|
228
|
-
is a `ValueError` out of an enum constructor. All three are the server sending
|
|
229
|
-
something this client cannot read, which is a failed request rather than a bug in the
|
|
230
|
-
caller's code, so all three become the one error type here.
|
|
231
|
-
|
|
232
|
-
The call does nothing but build one generated request, send it and hand the answer to
|
|
233
|
-
the generated decoder, which is what keeps this catch from swallowing a fault of our own.
|
|
234
|
-
An argument is converted before it, so a format this client does not publish is the
|
|
235
|
-
caller's `ValueError` rather than the server's failure.
|
|
236
|
-
"""
|
|
237
|
-
try:
|
|
238
|
-
return call()
|
|
239
|
-
except (KeyError, TypeError, ValueError) as exc:
|
|
240
|
-
raise malformed(exc) from exc
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
async def send_async(call: Callable[[], Awaitable[Response[Any]]]) -> Response[Any]:
|
|
244
|
-
"""`send`, awaited."""
|
|
245
|
-
try:
|
|
246
|
-
return await call()
|
|
247
|
-
except (KeyError, TypeError, ValueError) as exc:
|
|
248
|
-
raise malformed(exc) from exc
|
|
249
|
-
|
|
250
|
-
|
|
251
240
|
def request(
|
|
252
241
|
endpoint: ModuleType, client: AuthenticatedClient, bound: float | None, **params: Any
|
|
253
|
-
) -> Response
|
|
254
|
-
"""
|
|
255
|
-
|
|
242
|
+
) -> httpx.Response:
|
|
243
|
+
"""One attempt at one generated endpoint, finished within `bound` seconds, or unbounded
|
|
244
|
+
when that is None, and its answer checked by `decoded`.
|
|
256
245
|
|
|
257
246
|
Assembled from the endpoint module's `_get_kwargs` and `_build_response` because its
|
|
258
247
|
`sync_detailed` sends through httpx alone, and httpx has no bound on the attempt.
|
|
259
248
|
"""
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
req = http.build_request(**endpoint._get_kwargs(**params), timeout=httpx.Timeout(bound))
|
|
264
|
-
res = exchange(http, req, bound)
|
|
265
|
-
return cast(Response[Any], endpoint._build_response(client=client, response=res))
|
|
266
|
-
|
|
267
|
-
return send(call)
|
|
249
|
+
http = client.get_httpx_client()
|
|
250
|
+
req = http.build_request(**endpoint._get_kwargs(**params), timeout=httpx.Timeout(bound))
|
|
251
|
+
return decoded(endpoint, client, exchange(http, req, bound))
|
|
268
252
|
|
|
269
253
|
|
|
270
254
|
async def request_async(
|
|
271
255
|
endpoint: ModuleType, client: AuthenticatedClient, bound: float | None, **params: Any
|
|
272
|
-
) -> Response
|
|
256
|
+
) -> httpx.Response:
|
|
273
257
|
"""`request`, awaited."""
|
|
258
|
+
http = client.get_async_httpx_client()
|
|
259
|
+
req = http.build_request(**endpoint._get_kwargs(**params), timeout=httpx.Timeout(bound))
|
|
260
|
+
return decoded(endpoint, client, await exchange_async(http, req, bound))
|
|
274
261
|
|
|
275
|
-
async def call() -> Response[Any]:
|
|
276
|
-
http = client.get_async_httpx_client()
|
|
277
|
-
req = http.build_request(**endpoint._get_kwargs(**params), timeout=httpx.Timeout(bound))
|
|
278
|
-
res = await exchange_async(http, req, bound)
|
|
279
|
-
return cast(Response[Any], endpoint._build_response(client=client, response=res))
|
|
280
262
|
|
|
281
|
-
|
|
263
|
+
def decoded(
|
|
264
|
+
endpoint: ModuleType, client: AuthenticatedClient, res: httpx.Response
|
|
265
|
+
) -> httpx.Response:
|
|
266
|
+
"""`res`, once the generated decoder has read it, where it is a 2xx.
|
|
267
|
+
|
|
268
|
+
The generated code eagerly decodes the body of every DOCUMENTED status into its model,
|
|
269
|
+
and it raises three different ways doing it: a body that is not JSON is a `ValueError`
|
|
270
|
+
out of `json()`, an object missing a required key a `KeyError`, and a `standing` this
|
|
271
|
+
pinned spec predates a `ValueError` out of an enum constructor. On a 2xx each is the
|
|
272
|
+
server sending an answer this client cannot read, so each is the one error type here,
|
|
273
|
+
a retried `server_error` carrying the status.
|
|
274
|
+
|
|
275
|
+
Any other status is left to `unwrap`, which classifies it by the status alone, whatever
|
|
276
|
+
its body. Decoded too, a proxy's HTML page on a 401, a 404 or an empty 403 was a
|
|
277
|
+
retried `server_error` with no status, sent three times (2.7.1, measured 2026-10-12).
|
|
278
|
+
"""
|
|
279
|
+
if 200 <= res.status_code < 300:
|
|
280
|
+
try:
|
|
281
|
+
endpoint._build_response(client=client, response=res)
|
|
282
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
283
|
+
raise malformed(exc, res.status_code) from exc
|
|
284
|
+
return res
|
|
282
285
|
|
|
283
286
|
|
|
284
287
|
def exchange(http: httpx.Client, req: httpx.Request, bound: float | None) -> httpx.Response:
|
|
@@ -458,16 +461,16 @@ class AsyncClock:
|
|
|
458
461
|
await asyncio.sleep(seconds)
|
|
459
462
|
|
|
460
463
|
|
|
461
|
-
def malformed(exc: Exception) -> InternetDataError:
|
|
462
|
-
return InternetDataError("server_error", f"malformed response from the API: {exc}")
|
|
464
|
+
def malformed(exc: Exception, status: int) -> InternetDataError:
|
|
465
|
+
return InternetDataError("server_error", f"malformed response from the API: {exc}", status)
|
|
463
466
|
|
|
464
467
|
|
|
465
|
-
def unwrap(res: Response
|
|
468
|
+
def unwrap(res: httpx.Response) -> dict[str, Any]:
|
|
466
469
|
"""The response body as it came off the wire, or the failure it describes."""
|
|
467
470
|
body = _decode(res.content)
|
|
468
|
-
status =
|
|
471
|
+
status = res.status_code
|
|
469
472
|
if not 200 <= status < 300:
|
|
470
|
-
raise error_from_response(status,
|
|
473
|
+
raise error_from_response(status, res.headers, body)
|
|
471
474
|
if not isinstance(body, dict):
|
|
472
475
|
raise InternetDataError("server_error", "the API answered with a non-object body", status)
|
|
473
476
|
return body
|
|
@@ -484,23 +487,27 @@ def as_error(exc: InternetDataError | httpx.HTTPError) -> InternetDataError:
|
|
|
484
487
|
return InternetDataError("network", str(exc) or type(exc).__name__)
|
|
485
488
|
|
|
486
489
|
|
|
487
|
-
def parse_body(
|
|
490
|
+
def parse_body(res: httpx.Response, parse: Callable[[dict[str, Any]], T]) -> T:
|
|
488
491
|
"""A served body through the model layer, with a malformed one reported as the
|
|
489
|
-
server's failure rather than as a traceback out of a dataclass
|
|
492
|
+
server's failure, carrying its status, rather than as a traceback out of a dataclass
|
|
493
|
+
constructor."""
|
|
494
|
+
body = unwrap(res)
|
|
490
495
|
try:
|
|
491
496
|
return parse(body)
|
|
492
|
-
|
|
493
|
-
|
|
497
|
+
# AttributeError too: metadata's `schema` or `sample` as an array instead of an object
|
|
498
|
+
# escaped 2.7.1 raw, out of `.items()` (measured 2026-10-12).
|
|
499
|
+
except (AttributeError, KeyError, TypeError, ValueError) as exc:
|
|
500
|
+
raise malformed(exc, res.status_code) from exc
|
|
494
501
|
|
|
495
502
|
|
|
496
|
-
def redirect_location(res: Response
|
|
503
|
+
def redirect_location(res: httpx.Response) -> str:
|
|
497
504
|
"""Where a `302` points, or whatever the API said instead."""
|
|
498
|
-
location: str | None =
|
|
499
|
-
if
|
|
505
|
+
location: str | None = res.headers.get("location")
|
|
506
|
+
if res.status_code == 302 and location:
|
|
500
507
|
return location
|
|
501
508
|
unwrap(res)
|
|
502
509
|
raise InternetDataError(
|
|
503
|
-
"server_error", "expected a redirect to object storage",
|
|
510
|
+
"server_error", "expected a redirect to object storage", res.status_code
|
|
504
511
|
)
|
|
505
512
|
|
|
506
513
|
|
|
@@ -519,13 +526,18 @@ def downloads_of(body: dict[str, Any]) -> list[Download]:
|
|
|
519
526
|
|
|
520
527
|
|
|
521
528
|
def checksums_of(body: dict[str, Any]) -> dict[str, str]:
|
|
522
|
-
"""The digests, unwrapped one level past the envelope
|
|
529
|
+
"""The digests, unwrapped one level past the envelope, the four the spec requires
|
|
530
|
+
among them as strings.
|
|
523
531
|
|
|
524
532
|
The response carries `id` and `format` beside them, so reading a top-level `sha256`
|
|
525
533
|
finds nothing. That exact mistake shipped in another binding's 1.0.x, which is why
|
|
526
534
|
the shared corpus pins the depth.
|
|
527
535
|
"""
|
|
528
|
-
|
|
536
|
+
digests = dict(body["checksums"])
|
|
537
|
+
for algorithm in ("md5", "sha1", "sha256", "sha512"):
|
|
538
|
+
if not isinstance(digests[algorithm], str):
|
|
539
|
+
raise TypeError(f"the {algorithm} checksum is not a string: {digests[algorithm]!r}")
|
|
540
|
+
return digests
|
|
529
541
|
|
|
530
542
|
|
|
531
543
|
def retry_delay(err: InternetDataError, attempt: int, retries: int) -> float | None:
|
|
@@ -596,13 +608,6 @@ def _deadline_passed(bound: float) -> InternetDataError:
|
|
|
596
608
|
return InternetDataError("network", f"the request did not complete within {bound:g} seconds")
|
|
597
609
|
|
|
598
610
|
|
|
599
|
-
# The generated Response declares a plain MutableMapping, but always carries httpx's
|
|
600
|
-
# case-insensitive Headers. Rebuilding one keeps a header lookup case-blind whichever it
|
|
601
|
-
# turns out to be, which matters for `Retry-After`.
|
|
602
|
-
def _headers(res: Response[Any]) -> httpx.Headers:
|
|
603
|
-
return httpx.Headers(res.headers)
|
|
604
|
-
|
|
605
|
-
|
|
606
611
|
def _decode(content: bytes) -> Any:
|
|
607
612
|
try:
|
|
608
613
|
return json.loads(content)
|
|
@@ -59,7 +59,6 @@ def sync_detailed(
|
|
|
59
59
|
The whole published catalog, with your organization's license beside
|
|
60
60
|
each entry, so `standing` says whether a database is yours today
|
|
61
61
|
(`licensed`), was (`expired`), or has never been bought (`unlicensed`).
|
|
62
|
-
An `open` database downloads whatever its `standing`.
|
|
63
62
|
|
|
64
63
|
Raises:
|
|
65
64
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
@@ -87,7 +86,6 @@ def sync(
|
|
|
87
86
|
The whole published catalog, with your organization's license beside
|
|
88
87
|
each entry, so `standing` says whether a database is yours today
|
|
89
88
|
(`licensed`), was (`expired`), or has never been bought (`unlicensed`).
|
|
90
|
-
An `open` database downloads whatever its `standing`.
|
|
91
89
|
|
|
92
90
|
Raises:
|
|
93
91
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
@@ -111,7 +109,6 @@ async def asyncio_detailed(
|
|
|
111
109
|
The whole published catalog, with your organization's license beside
|
|
112
110
|
each entry, so `standing` says whether a database is yours today
|
|
113
111
|
(`licensed`), was (`expired`), or has never been bought (`unlicensed`).
|
|
114
|
-
An `open` database downloads whatever its `standing`.
|
|
115
112
|
|
|
116
113
|
Raises:
|
|
117
114
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
@@ -137,7 +134,6 @@ async def asyncio(
|
|
|
137
134
|
The whole published catalog, with your organization's license beside
|
|
138
135
|
each entry, so `standing` says whether a database is yours today
|
|
139
136
|
(`licensed`), was (`expired`), or has never been bought (`unlicensed`).
|
|
140
|
-
An `open` database downloads whatever its `standing`.
|
|
141
137
|
|
|
142
138
|
Raises:
|
|
143
139
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
@@ -31,13 +31,12 @@ class Database:
|
|
|
31
31
|
summary (str): One line on what the newest version contains.
|
|
32
32
|
standing (Standing): Where your license for a database family stands today. `licensed` is a
|
|
33
33
|
live grant, `expired` one whose term has ended, and `unlicensed` a
|
|
34
|
-
database published but never bought.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
own license, which grants more where you hold one.
|
|
34
|
+
database published but never bought.
|
|
35
|
+
open_ (bool): An Open database: free under CC BY-SA 4.0 (credit InternetData,
|
|
36
|
+
https://internetdata.io, and share what you build from it under
|
|
37
|
+
the same license). Every organization holds a `standard` license
|
|
38
|
+
to it from the start, which `standing` and `license_type` report
|
|
39
|
+
like any other.
|
|
41
40
|
license_type (DatabaseLicenseTypeType1 | DatabaseLicenseTypeType2Type1 | DatabaseLicenseTypeType3Type1 | None):
|
|
42
41
|
What your license permits you to do with the data. Null when there
|
|
43
42
|
is no license, which is every family with standing `unlicensed`.
|
|
@@ -21,9 +21,7 @@ class Download:
|
|
|
21
21
|
format_ (str):
|
|
22
22
|
outcome (DownloadOutcome):
|
|
23
23
|
sample (bool): The evaluation sample rather than the database itself.
|
|
24
|
-
open_ (bool):
|
|
25
|
-
Open database downloaded while your organization held no license
|
|
26
|
-
in term for it.
|
|
24
|
+
open_ (bool): A download of an Open database.
|
|
27
25
|
bytes_ (int | None): Object size at redirect time, NOT bytes delivered: the transfer is a
|
|
28
26
|
presigned redirect straight to object storage, so we never observe
|
|
29
27
|
how much of it was taken.
|
|
@@ -48,7 +48,6 @@ from ._core import (
|
|
|
48
48
|
request_async,
|
|
49
49
|
retry_delay,
|
|
50
50
|
storage_refusal,
|
|
51
|
-
unwrap,
|
|
52
51
|
)
|
|
53
52
|
from ._generated.api.database_v_2 import (
|
|
54
53
|
database_checksum_v2,
|
|
@@ -172,7 +171,7 @@ class AsyncDatabaseApi:
|
|
|
172
171
|
|
|
173
172
|
async def call() -> builtins.list[Database]:
|
|
174
173
|
res = await request_async(list_databases, self._client, self._bound(timeout))
|
|
175
|
-
return parse_body(
|
|
174
|
+
return parse_body(res, databases_of)
|
|
176
175
|
|
|
177
176
|
return await self._retrying(call)
|
|
178
177
|
|
|
@@ -186,7 +185,7 @@ class AsyncDatabaseApi:
|
|
|
186
185
|
res = await request_async(
|
|
187
186
|
database_metadata_v2, self._client, self._bound(timeout), id=database_id
|
|
188
187
|
)
|
|
189
|
-
return parse_body(
|
|
188
|
+
return parse_body(res, to_metadata)
|
|
190
189
|
|
|
191
190
|
return await self._retrying(call)
|
|
192
191
|
|
|
@@ -206,7 +205,7 @@ class AsyncDatabaseApi:
|
|
|
206
205
|
id=database_id,
|
|
207
206
|
format_=DatabaseFormat(format),
|
|
208
207
|
)
|
|
209
|
-
return parse_body(
|
|
208
|
+
return parse_body(res, checksums_of)
|
|
210
209
|
|
|
211
210
|
return await self._retrying(call)
|
|
212
211
|
|
|
@@ -222,7 +221,7 @@ class AsyncDatabaseApi:
|
|
|
222
221
|
res = await request_async(
|
|
223
222
|
list_downloads, self._client, self._bound(timeout), limit=limit
|
|
224
223
|
)
|
|
225
|
-
return parse_body(
|
|
224
|
+
return parse_body(res, downloads_of)
|
|
226
225
|
|
|
227
226
|
return await self._retrying(call)
|
|
228
227
|
|
|
@@ -43,7 +43,6 @@ from ._core import (
|
|
|
43
43
|
request,
|
|
44
44
|
retry_delay,
|
|
45
45
|
storage_refusal,
|
|
46
|
-
unwrap,
|
|
47
46
|
)
|
|
48
47
|
from ._generated.api.database_v_2 import (
|
|
49
48
|
database_checksum_v2,
|
|
@@ -184,7 +183,7 @@ class DatabaseApi:
|
|
|
184
183
|
|
|
185
184
|
def call() -> builtins.list[Database]:
|
|
186
185
|
res = request(list_databases, self._client, self._bound(timeout))
|
|
187
|
-
return parse_body(
|
|
186
|
+
return parse_body(res, databases_of)
|
|
188
187
|
|
|
189
188
|
return self._retrying(call)
|
|
190
189
|
|
|
@@ -199,7 +198,7 @@ class DatabaseApi:
|
|
|
199
198
|
|
|
200
199
|
def call() -> DatabaseMetadata:
|
|
201
200
|
res = request(database_metadata_v2, self._client, self._bound(timeout), id=database_id)
|
|
202
|
-
return parse_body(
|
|
201
|
+
return parse_body(res, to_metadata)
|
|
203
202
|
|
|
204
203
|
return self._retrying(call)
|
|
205
204
|
|
|
@@ -222,7 +221,7 @@ class DatabaseApi:
|
|
|
222
221
|
id=database_id,
|
|
223
222
|
format_=DatabaseFormat(format),
|
|
224
223
|
)
|
|
225
|
-
return parse_body(
|
|
224
|
+
return parse_body(res, checksums_of)
|
|
226
225
|
|
|
227
226
|
return self._retrying(call)
|
|
228
227
|
|
|
@@ -239,7 +238,7 @@ class DatabaseApi:
|
|
|
239
238
|
|
|
240
239
|
def call() -> builtins.list[Download]:
|
|
241
240
|
res = request(list_downloads, self._client, self._bound(timeout), limit=limit)
|
|
242
|
-
return parse_body(
|
|
241
|
+
return parse_body(res, downloads_of)
|
|
243
242
|
|
|
244
243
|
return self._retrying(call)
|
|
245
244
|
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
|
-
import email.utils
|
|
7
6
|
import math
|
|
7
|
+
import re
|
|
8
8
|
from typing import Any, Literal
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
@@ -170,21 +170,65 @@ def _message_of(body: Any) -> str | None:
|
|
|
170
170
|
return None
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
# `Retry-After` is a count of seconds or an HTTP date in one of RFC 9110's three forms,
|
|
174
|
+
# always GMT, and nothing else: `float()` also read `+1`, `1_0`, `1e3` and `1.5` as counts,
|
|
175
|
+
# and `email.utils` read a date in any zone, without its day name or with a two-digit year,
|
|
176
|
+
# so `1_0` held a call ten seconds and a `PST` date eight hours (2.7.1, measured 2026-10-12).
|
|
177
|
+
_SECONDS = re.compile(r"[0-9]+")
|
|
178
|
+
_MONTH = r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
|
|
179
|
+
_CLOCK = r"([0-9]{2}):([0-9]{2}):([0-9]{2})"
|
|
180
|
+
_IMF_FIXDATE = re.compile(
|
|
181
|
+
rf"(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), ([0-9]{{2}}) {_MONTH} ([0-9]{{4}}) {_CLOCK} GMT"
|
|
182
|
+
)
|
|
183
|
+
_RFC850_DATE = re.compile(
|
|
184
|
+
rf"(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), ([0-9]{{2}})-{_MONTH}-([0-9]{{2}}) {_CLOCK} GMT"
|
|
185
|
+
)
|
|
186
|
+
_ASCTIME_DATE = re.compile(
|
|
187
|
+
rf"(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) {_MONTH} ([0-9]{{2}}| [0-9]) {_CLOCK} ([0-9]{{4}})"
|
|
188
|
+
)
|
|
189
|
+
_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
|
190
|
+
|
|
191
|
+
|
|
173
192
|
def _parse_retry_after(value: str | None) -> float | None:
|
|
174
|
-
if value is None
|
|
193
|
+
if value is None:
|
|
175
194
|
return None
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
# The header also permits an HTTP date.
|
|
183
|
-
try:
|
|
184
|
-
when = email.utils.parsedate_to_datetime(value)
|
|
185
|
-
except (TypeError, ValueError):
|
|
195
|
+
value = value.strip(" \t")
|
|
196
|
+
if _SECONDS.fullmatch(value):
|
|
197
|
+
# A count of any length: float() reads one past its range as inf, never raising.
|
|
198
|
+
return float(value)
|
|
199
|
+
when = _http_date(value)
|
|
200
|
+
if when is None:
|
|
186
201
|
return None
|
|
187
|
-
if when.tzinfo is None:
|
|
188
|
-
when = when.replace(tzinfo=datetime.UTC)
|
|
189
202
|
delta = (when - datetime.datetime.now(datetime.UTC)).total_seconds()
|
|
190
203
|
return max(0.0, math.ceil(delta))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# The moment an HTTP date names, or None for anything else, a day no calendar has included.
|
|
207
|
+
def _http_date(value: str) -> datetime.datetime | None:
|
|
208
|
+
if match := _IMF_FIXDATE.fullmatch(value):
|
|
209
|
+
day, month, year, hour, minute, second = match.groups()
|
|
210
|
+
full_year = int(year)
|
|
211
|
+
elif match := _RFC850_DATE.fullmatch(value):
|
|
212
|
+
day, month, year, hour, minute, second = match.groups()
|
|
213
|
+
# RFC 9110: a two-digit year more than 50 years ahead is the most recent past one.
|
|
214
|
+
this_year = datetime.datetime.now(datetime.UTC).year
|
|
215
|
+
full_year = this_year - this_year % 100 + int(year)
|
|
216
|
+
if full_year > this_year + 50:
|
|
217
|
+
full_year -= 100
|
|
218
|
+
elif match := _ASCTIME_DATE.fullmatch(value):
|
|
219
|
+
month, day, hour, minute, second, year = match.groups()
|
|
220
|
+
full_year = int(year)
|
|
221
|
+
else:
|
|
222
|
+
return None
|
|
223
|
+
try:
|
|
224
|
+
return datetime.datetime(
|
|
225
|
+
full_year,
|
|
226
|
+
_MONTHS.index(month) + 1,
|
|
227
|
+
int(day),
|
|
228
|
+
int(hour),
|
|
229
|
+
int(minute),
|
|
230
|
+
int(second),
|
|
231
|
+
tzinfo=datetime.UTC,
|
|
232
|
+
)
|
|
233
|
+
except ValueError:
|
|
234
|
+
return None
|