internetdata 2.9.2__tar.gz → 2.9.4__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.9.2 → internetdata-2.9.4}/PKG-INFO +3 -3
- {internetdata-2.9.2 → internetdata-2.9.4}/README.md +2 -2
- {internetdata-2.9.2 → internetdata-2.9.4}/spec/openapi.yaml +8 -8
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/__init__.py +1 -1
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_core.py +46 -6
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/api_keys/account_rotate_apikey.py +4 -4
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database.py +1 -1
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_version.py +2 -2
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/aio.py +3 -2
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/client.py +8 -3
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/errors.py +1 -1
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/models.py +91 -30
- {internetdata-2.9.2 → internetdata-2.9.4}/tests/test_client.py +153 -2
- {internetdata-2.9.2 → internetdata-2.9.4}/tests/test_conformance.py +1 -1
- {internetdata-2.9.2 → internetdata-2.9.4}/.gitignore +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/LICENSE +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/pyproject.toml +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/scripts/download-spec.sh +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/scripts/generate.sh +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/scripts/publish.sh +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/scripts/v2_subset.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/__init__.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/__init__.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/account/__init__.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/account/account_identity.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/api_keys/__init__.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/api_keys/account_create_apikey.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/api_keys/account_list_apikeys.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/api_keys/account_reveal_apikey.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/api_keys/account_revoke_apikey.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/authorization/__init__.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/authorization/oauth_authorize.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/authorization/oauth_device_authorization.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/authorization/oauth_metadata.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/authorization/oauth_register.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/authorization/oauth_revoke.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/authorization/oauth_token.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/database_v_2/__init__.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/database_v_2/database_checksum_v2.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/database_v_2/database_metadata_v2.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/database_v_2/download_database_v2.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/database_v_2/list_databases.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/database_v_2/list_downloads.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/organization/__init__.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/organization/account_org.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/organization/account_org_members.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/client.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/errors.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/__init__.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_create_apikey_request.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_created_apikey.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_org.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_org_ref.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_org_wrap.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_rc.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_revealed_apikey.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_user.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/apikey_detail.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/apikey_list.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/client_registration.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/client_registration_request.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_checksum_v2_response_200.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_format.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_license_type_type_1.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_license_type_type_2_type_1.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_license_type_type_3_type_1.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_metadata.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_metadata_column.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_metadata_sample.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_metadata_sample_additional_property_item.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_metadata_sample_size.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_metadata_schema.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_metadata_size.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/db_checksums.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/device_authorization.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/device_authorization_request.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/download.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/download_outcome.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/error.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/identity.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/list_databases_response_200.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/list_downloads_response_200.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/oauth_authorize_code_challenge_method.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/oauth_authorize_response_type.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/oauth_error.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/oauth_metadata.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/oauth_revoke_response_200.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/revoke_request.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/standing.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/token_request.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/token_response.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/types.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/py.typed +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/testdata/testdata.json +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/tests/conftest.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/tests/helpers.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/tests/test_download.py +0 -0
- {internetdata-2.9.2 → internetdata-2.9.4}/tests/test_oauth.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: internetdata
|
|
3
|
-
Version: 2.9.
|
|
3
|
+
Version: 2.9.4
|
|
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
|
|
@@ -72,7 +72,7 @@ with InternetData(api_key) as client:
|
|
|
72
72
|
|
|
73
73
|
### The catalog
|
|
74
74
|
|
|
75
|
-
`list` answers database FAMILIES. A license is held against a family, while a download names a specific version, so the
|
|
75
|
+
`list` answers database FAMILIES. A license is held against a family, while a download names a specific version, so the IDs the other calls take come from each family's `versions`:
|
|
76
76
|
|
|
77
77
|
```python
|
|
78
78
|
for family in client.database.list():
|
|
@@ -180,7 +180,7 @@ From 2.3.0, `list`, `metadata`, `checksums`, `downloads` and `download_url` each
|
|
|
180
180
|
|
|
181
181
|
**Changed in 2.2.0:** a timeout of 0 or less, NaN or a string used to be accepted, and failed every call.
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
`rate_limited` is a 429 that carries `Retry-After`, and the library retries it for you, as it does server and network failures. `quota_exceeded` is a 429 without one, which it never retries, and it retries nothing else you sent either. InternetData's database API has no quota, so you won't see `quota_exceeded` from it.
|
|
184
184
|
|
|
185
185
|
### Sign in with OAuth (device flow)
|
|
186
186
|
|
|
@@ -38,7 +38,7 @@ with InternetData(api_key) as client:
|
|
|
38
38
|
|
|
39
39
|
### The catalog
|
|
40
40
|
|
|
41
|
-
`list` answers database FAMILIES. A license is held against a family, while a download names a specific version, so the
|
|
41
|
+
`list` answers database FAMILIES. A license is held against a family, while a download names a specific version, so the IDs the other calls take come from each family's `versions`:
|
|
42
42
|
|
|
43
43
|
```python
|
|
44
44
|
for family in client.database.list():
|
|
@@ -146,7 +146,7 @@ From 2.3.0, `list`, `metadata`, `checksums`, `downloads` and `download_url` each
|
|
|
146
146
|
|
|
147
147
|
**Changed in 2.2.0:** a timeout of 0 or less, NaN or a string used to be accepted, and failed every call.
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
`rate_limited` is a 429 that carries `Retry-After`, and the library retries it for you, as it does server and network failures. `quota_exceeded` is a 429 without one, which it never retries, and it retries nothing else you sent either. InternetData's database API has no quota, so you won't see `quota_exceeded` from it.
|
|
150
150
|
|
|
151
151
|
### Sign in with OAuth (device flow)
|
|
152
152
|
|
|
@@ -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.11.02
|
|
16
16
|
servers:
|
|
17
17
|
- url: https://internetdata.io
|
|
18
18
|
tags:
|
|
@@ -506,7 +506,7 @@ paths:
|
|
|
506
506
|
- API keys
|
|
507
507
|
summary: Rotate
|
|
508
508
|
description: |
|
|
509
|
-
Replaces the secret behind a key, keeping its
|
|
509
|
+
Replaces the secret behind a key, keeping its ID, name and settings.
|
|
510
510
|
The previous secret stops working immediately. The new secret comes
|
|
511
511
|
back in the answer, so this needs `apikeys.reveal` as well as
|
|
512
512
|
`apikeys.manage`.
|
|
@@ -890,7 +890,7 @@ components:
|
|
|
890
890
|
name: apikey
|
|
891
891
|
description: |
|
|
892
892
|
UUID-format API key issued to the customer. Each key is authorized for a
|
|
893
|
-
specific set of database
|
|
893
|
+
specific set of database IDs; requesting any other ID returns 403.
|
|
894
894
|
|
|
895
895
|
v1 only. The v2 endpoints take a console-issued key as a bearer token.
|
|
896
896
|
BearerAuth:
|
|
@@ -936,7 +936,7 @@ components:
|
|
|
936
936
|
name: id
|
|
937
937
|
in: path
|
|
938
938
|
required: true
|
|
939
|
-
description: The key's
|
|
939
|
+
description: The key's ID, as returned by the list endpoint. Never the key itself.
|
|
940
940
|
schema:
|
|
941
941
|
type: string
|
|
942
942
|
format: uuid
|
|
@@ -973,7 +973,7 @@ components:
|
|
|
973
973
|
description: |
|
|
974
974
|
One database FAMILY, with your organization's license beside it. A
|
|
975
975
|
license covers the family, while a download names a specific version,
|
|
976
|
-
so the
|
|
976
|
+
so the IDs passed to `download` and `checksum` come from `versions`.
|
|
977
977
|
required:
|
|
978
978
|
- base
|
|
979
979
|
- name
|
|
@@ -1061,7 +1061,7 @@ components:
|
|
|
1061
1061
|
properties:
|
|
1062
1062
|
id:
|
|
1063
1063
|
type: string
|
|
1064
|
-
description: The versioned
|
|
1064
|
+
description: The versioned ID. Pass this to `download`, `checksum` and `metadata`.
|
|
1065
1065
|
example: vpn_ip_v1
|
|
1066
1066
|
version:
|
|
1067
1067
|
type: integer
|
|
@@ -1072,7 +1072,7 @@ components:
|
|
|
1072
1072
|
type: array
|
|
1073
1073
|
description: |
|
|
1074
1074
|
The formats this version is BUILT in. Asking for another is a 400,
|
|
1075
|
-
not a gap - the `_provider` catalogs are keyed by provider
|
|
1075
|
+
not a gap - the `_provider` catalogs are keyed by provider ID, so no
|
|
1076
1076
|
MMDB exists for them.
|
|
1077
1077
|
items:
|
|
1078
1078
|
$ref: '#/components/schemas/DatabaseFormat'
|
|
@@ -1826,7 +1826,7 @@ components:
|
|
|
1826
1826
|
schema:
|
|
1827
1827
|
$ref: '#/components/schemas/DbInvalidApiKeyError'
|
|
1828
1828
|
Forbidden:
|
|
1829
|
-
description: The `apikey` is valid but is not authorized for the requested database
|
|
1829
|
+
description: The `apikey` is valid but is not authorized for the requested database ID.
|
|
1830
1830
|
content:
|
|
1831
1831
|
application/json:
|
|
1832
1832
|
schema:
|
|
@@ -167,15 +167,17 @@ def check_timeout(timeout: float | None) -> float | None:
|
|
|
167
167
|
`server_error` blaming the API. So is anything past `WAIT_CEILING`, infinity
|
|
168
168
|
included, since the sync client's wait cannot hold it: 1e300 failed every call with
|
|
169
169
|
a raw `OverflowError` (2.6.0, measured 2026-09-30). None is the spelling for no bound.
|
|
170
|
+
|
|
171
|
+
Compared rather than handed to `math.isfinite`, which raised a raw `OverflowError` for
|
|
172
|
+
an integer too large for a float, such as 10**400 (2.9.2, measured 2026-10-25). NaN
|
|
173
|
+
fails both comparisons.
|
|
170
174
|
"""
|
|
171
175
|
if timeout is None:
|
|
172
176
|
return None
|
|
173
177
|
if (
|
|
174
178
|
isinstance(timeout, bool)
|
|
175
179
|
or not isinstance(timeout, int | float)
|
|
176
|
-
or not
|
|
177
|
-
or timeout <= 0
|
|
178
|
-
or timeout > WAIT_CEILING
|
|
180
|
+
or not 0 < timeout <= WAIT_CEILING
|
|
179
181
|
):
|
|
180
182
|
raise ValueError(
|
|
181
183
|
f"timeout must be a number of seconds greater than 0 and at most "
|
|
@@ -184,6 +186,25 @@ def check_timeout(timeout: float | None) -> float | None:
|
|
|
184
186
|
return timeout
|
|
185
187
|
|
|
186
188
|
|
|
189
|
+
def check_retries(retries: float) -> float:
|
|
190
|
+
"""`retries`, once it is a count of retries: a whole number from 0, or `math.inf` to
|
|
191
|
+
retry a failing call until it succeeds.
|
|
192
|
+
|
|
193
|
+
Refused where it is SET, because nothing downstream refuses it: NaN compares false with
|
|
194
|
+
every attempt number, so a failing call was retried until it succeeded, a negative read
|
|
195
|
+
as 0, 1.5 as two retries, and a string or None raised a raw `TypeError` out of every
|
|
196
|
+
call that failed, a 404 included (2.9.2, measured 2026-10-25).
|
|
197
|
+
"""
|
|
198
|
+
if (
|
|
199
|
+
isinstance(retries, bool)
|
|
200
|
+
or not isinstance(retries, int | float)
|
|
201
|
+
or not retries >= 0
|
|
202
|
+
or (isinstance(retries, float) and not (retries.is_integer() or retries == math.inf))
|
|
203
|
+
):
|
|
204
|
+
raise ValueError(f"retries must be a whole number from 0, or math.inf, not {retries!r}")
|
|
205
|
+
return retries
|
|
206
|
+
|
|
207
|
+
|
|
187
208
|
def storage_refusal(res: httpx.Response) -> InternetDataError:
|
|
188
209
|
"""What object storage refusing a download link becomes.
|
|
189
210
|
|
|
@@ -553,12 +574,22 @@ def databases_of(body: dict[str, Any]) -> list[Database]:
|
|
|
553
574
|
Nothing is added here, and nothing may be: a family built for a single customer is
|
|
554
575
|
absent from this array for every organization that does not license it, so filling a
|
|
555
576
|
gap from any other source would publish the fact that the family exists.
|
|
577
|
+
|
|
578
|
+
An object where the list is due is refused rather than read as one with nothing in it:
|
|
579
|
+
`{"databases": {}}` answered an empty catalog (2.9.2, measured 2026-10-25).
|
|
556
580
|
"""
|
|
557
|
-
return [to_database(d) for d in body
|
|
581
|
+
return [to_database(d) for d in _listed(body, "databases")]
|
|
558
582
|
|
|
559
583
|
|
|
560
584
|
def downloads_of(body: dict[str, Any]) -> list[Download]:
|
|
561
|
-
return [to_download(d) for d in body
|
|
585
|
+
return [to_download(d) for d in _listed(body, "downloads")]
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _listed(body: dict[str, Any], name: str) -> list[Any]:
|
|
589
|
+
value = body[name]
|
|
590
|
+
if not isinstance(value, list):
|
|
591
|
+
raise TypeError(f"{name} is not a list: {value!r}")
|
|
592
|
+
return value
|
|
562
593
|
|
|
563
594
|
|
|
564
595
|
def checksums_of(body: dict[str, Any]) -> dict[str, str]:
|
|
@@ -568,7 +599,16 @@ def checksums_of(body: dict[str, Any]) -> dict[str, str]:
|
|
|
568
599
|
The response carries `id` and `format` beside them, so reading a top-level `sha256`
|
|
569
600
|
finds nothing. That exact mistake shipped in another binding's 1.0.x, which is why
|
|
570
601
|
the shared corpus pins the depth.
|
|
602
|
+
|
|
603
|
+
`id` and `format` are read too, though nothing returns them: a null or numeric `id`
|
|
604
|
+
came back as a whole answer, and so did `checksums` sent as a list of pairs (2.9.2,
|
|
605
|
+
measured 2026-10-25).
|
|
571
606
|
"""
|
|
607
|
+
for member in ("id", "format"):
|
|
608
|
+
if not isinstance(body[member], str):
|
|
609
|
+
raise TypeError(f"the checksum answer's {member} is not a string: {body[member]!r}")
|
|
610
|
+
if not isinstance(body["checksums"], dict):
|
|
611
|
+
raise TypeError(f"checksums is not an object: {body['checksums']!r}")
|
|
572
612
|
digests = dict(body["checksums"])
|
|
573
613
|
for algorithm in ("md5", "sha1", "sha256", "sha512"):
|
|
574
614
|
if not isinstance(digests[algorithm], str):
|
|
@@ -576,7 +616,7 @@ def checksums_of(body: dict[str, Any]) -> dict[str, str]:
|
|
|
576
616
|
return digests
|
|
577
617
|
|
|
578
618
|
|
|
579
|
-
def retry_delay(err: InternetDataError, attempt: int, retries:
|
|
619
|
+
def retry_delay(err: InternetDataError, attempt: int, retries: float) -> float | None:
|
|
580
620
|
"""How long to wait before attempt `attempt + 1`, or None when there must not be one.
|
|
581
621
|
|
|
582
622
|
A server-supplied `Retry-After` wins over the backoff schedule outright: it is the
|
|
@@ -73,7 +73,7 @@ def sync_detailed(
|
|
|
73
73
|
) -> Response[AccountCreatedApikey | AccountRc]:
|
|
74
74
|
"""Rotate
|
|
75
75
|
|
|
76
|
-
Replaces the secret behind a key, keeping its
|
|
76
|
+
Replaces the secret behind a key, keeping its ID, name and settings.
|
|
77
77
|
The previous secret stops working immediately. The new secret comes
|
|
78
78
|
back in the answer, so this needs `apikeys.reveal` as well as
|
|
79
79
|
`apikeys.manage`.
|
|
@@ -107,7 +107,7 @@ def sync(
|
|
|
107
107
|
) -> AccountCreatedApikey | AccountRc | None:
|
|
108
108
|
"""Rotate
|
|
109
109
|
|
|
110
|
-
Replaces the secret behind a key, keeping its
|
|
110
|
+
Replaces the secret behind a key, keeping its ID, name and settings.
|
|
111
111
|
The previous secret stops working immediately. The new secret comes
|
|
112
112
|
back in the answer, so this needs `apikeys.reveal` as well as
|
|
113
113
|
`apikeys.manage`.
|
|
@@ -136,7 +136,7 @@ async def asyncio_detailed(
|
|
|
136
136
|
) -> Response[AccountCreatedApikey | AccountRc]:
|
|
137
137
|
"""Rotate
|
|
138
138
|
|
|
139
|
-
Replaces the secret behind a key, keeping its
|
|
139
|
+
Replaces the secret behind a key, keeping its ID, name and settings.
|
|
140
140
|
The previous secret stops working immediately. The new secret comes
|
|
141
141
|
back in the answer, so this needs `apikeys.reveal` as well as
|
|
142
142
|
`apikeys.manage`.
|
|
@@ -168,7 +168,7 @@ async def asyncio(
|
|
|
168
168
|
) -> AccountCreatedApikey | AccountRc | None:
|
|
169
169
|
"""Rotate
|
|
170
170
|
|
|
171
|
-
Replaces the secret behind a key, keeping its
|
|
171
|
+
Replaces the secret behind a key, keeping its ID, name and settings.
|
|
172
172
|
The previous secret stops working immediately. The new secret comes
|
|
173
173
|
back in the answer, so this needs `apikeys.reveal` as well as
|
|
174
174
|
`apikeys.manage`.
|
|
@@ -23,7 +23,7 @@ T = TypeVar("T", bound="Database")
|
|
|
23
23
|
class Database:
|
|
24
24
|
"""One database FAMILY, with your organization's license beside it. A
|
|
25
25
|
license covers the family, while a download names a specific version,
|
|
26
|
-
so the
|
|
26
|
+
so the IDs passed to `download` and `checksum` come from `versions`.
|
|
27
27
|
|
|
28
28
|
Attributes:
|
|
29
29
|
base (str): The family, e.g. `vpn_ip`. What a license is held against. Example: vpn_ip.
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_version.py
RENAMED
|
@@ -16,11 +16,11 @@ T = TypeVar("T", bound="DatabaseVersion")
|
|
|
16
16
|
class DatabaseVersion:
|
|
17
17
|
"""
|
|
18
18
|
Attributes:
|
|
19
|
-
id (str): The versioned
|
|
19
|
+
id (str): The versioned ID. Pass this to `download`, `checksum` and `metadata`. Example: vpn_ip_v1.
|
|
20
20
|
version (int): Example: 1.
|
|
21
21
|
summary (str):
|
|
22
22
|
formats (list[DatabaseFormat]): The formats this version is BUILT in. Asking for another is a 400,
|
|
23
|
-
not a gap - the `_provider` catalogs are keyed by provider
|
|
23
|
+
not a gap - the `_provider` catalogs are keyed by provider ID, so no
|
|
24
24
|
MMDB exists for them.
|
|
25
25
|
sample_formats (list[DatabaseFormat] | Unset): The formats an evaluation sample is published in, if any.
|
|
26
26
|
"""
|
|
@@ -35,6 +35,7 @@ from ._core import (
|
|
|
35
35
|
authorization_url,
|
|
36
36
|
build_async_transfer_client,
|
|
37
37
|
build_client,
|
|
38
|
+
check_retries,
|
|
38
39
|
check_timeout,
|
|
39
40
|
checksums_of,
|
|
40
41
|
create_pkce,
|
|
@@ -109,7 +110,7 @@ class AsyncInternetData:
|
|
|
109
110
|
self._client = build_client(api_key, base_url, timeout, transport)
|
|
110
111
|
self._base_url = base_url
|
|
111
112
|
self._transfer = build_async_transfer_client(timeout, transport)
|
|
112
|
-
self._retries = retries
|
|
113
|
+
self._retries = check_retries(retries)
|
|
113
114
|
self._timeout = timeout
|
|
114
115
|
self.database = AsyncDatabaseApi(self)
|
|
115
116
|
self.oauth = AsyncOauthApi(self)
|
|
@@ -133,7 +134,7 @@ class AsyncInternetData:
|
|
|
133
134
|
def _bound(self, timeout: float | None) -> float | None:
|
|
134
135
|
return self._timeout if timeout is None else check_timeout(timeout)
|
|
135
136
|
|
|
136
|
-
async def _retrying(self, call: Callable[[], Awaitable[T]], retries:
|
|
137
|
+
async def _retrying(self, call: Callable[[], Awaitable[T]], retries: float) -> T:
|
|
137
138
|
attempt = 0
|
|
138
139
|
while True:
|
|
139
140
|
try:
|
|
@@ -30,6 +30,7 @@ from ._core import (
|
|
|
30
30
|
authorization_url,
|
|
31
31
|
build_client,
|
|
32
32
|
build_transfer_client,
|
|
33
|
+
check_retries,
|
|
33
34
|
check_timeout,
|
|
34
35
|
checksums_of,
|
|
35
36
|
create_pkce,
|
|
@@ -94,6 +95,10 @@ class InternetData:
|
|
|
94
95
|
Anything else that is not a finite number greater than 0 is a `ValueError` where it is
|
|
95
96
|
set, rather than a failure of every call.
|
|
96
97
|
|
|
98
|
+
`retries` is how many times a failed attempt is retried, 2 unless given: a whole number
|
|
99
|
+
from 0, or `math.inf` to retry until the call succeeds. Anything else is a `ValueError`
|
|
100
|
+
here too.
|
|
101
|
+
|
|
97
102
|
Holds an HTTP connection pool, so use it as a context manager or call `close()` when
|
|
98
103
|
you are done with it.
|
|
99
104
|
"""
|
|
@@ -117,7 +122,7 @@ class InternetData:
|
|
|
117
122
|
self._client = build_client(api_key, base_url, timeout, transport)
|
|
118
123
|
self._base_url = base_url
|
|
119
124
|
self._transfer = build_transfer_client(timeout, transport)
|
|
120
|
-
self._retries = retries
|
|
125
|
+
self._retries = check_retries(retries)
|
|
121
126
|
self._timeout = timeout
|
|
122
127
|
self.database = DatabaseApi(self)
|
|
123
128
|
self.oauth = OauthApi(self)
|
|
@@ -141,7 +146,7 @@ class InternetData:
|
|
|
141
146
|
def _bound(self, timeout: float | None) -> float | None:
|
|
142
147
|
return self._timeout if timeout is None else check_timeout(timeout)
|
|
143
148
|
|
|
144
|
-
def _retrying(self, call: Callable[[], T], retries:
|
|
149
|
+
def _retrying(self, call: Callable[[], T], retries: float) -> T:
|
|
145
150
|
attempt = 0
|
|
146
151
|
while True:
|
|
147
152
|
try:
|
|
@@ -179,7 +184,7 @@ class DatabaseApi:
|
|
|
179
184
|
for the key you are holding, so a listing held from one key is not an answer for
|
|
180
185
|
another.
|
|
181
186
|
|
|
182
|
-
A license covers a FAMILY, while a download names a version, so the
|
|
187
|
+
A license covers a FAMILY, while a download names a version, so the IDs for the
|
|
183
188
|
other calls come from each entry's `versions`.
|
|
184
189
|
|
|
185
190
|
`timeout` bounds each attempt at this call alone, in place of the client's.
|
|
@@ -128,7 +128,7 @@ def error_from_response(status: int, headers: httpx.Headers, body: Any) -> Inter
|
|
|
128
128
|
"""Classify a failed response.
|
|
129
129
|
|
|
130
130
|
Everything outside the four named statuses is decided by its RANGE rather than by an
|
|
131
|
-
enumerated list, which is the whole point: a 404 from a database
|
|
131
|
+
enumerated list, which is the whole point: a 404 from a database ID that does not
|
|
132
132
|
exist has to land on a client error, and mapping 400/401/403/429 and letting the rest
|
|
133
133
|
fall through to `server_error` retries it twice before failing. Three of the four
|
|
134
134
|
first SDKs shipped exactly that bug, so the shared corpus pins both 404 shapes.
|
|
@@ -10,6 +10,7 @@ them so a field this pinned spec predates is still reachable.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
13
|
+
import re
|
|
13
14
|
from dataclasses import dataclass, field
|
|
14
15
|
from typing import Any, Literal, get_args
|
|
15
16
|
|
|
@@ -38,7 +39,7 @@ Format = Literal["csvgz", "mmdb"]
|
|
|
38
39
|
|
|
39
40
|
Which of them a given version HAS is `DatabaseVersion.formats`; asking for another is a
|
|
40
41
|
`bad_request` rather than a gap, because the `_provider` catalogs are keyed by provider
|
|
41
|
-
|
|
42
|
+
ID and no MMDB exists for them.
|
|
42
43
|
"""
|
|
43
44
|
|
|
44
45
|
Standing = Literal["licensed", "expired", "unlicensed"]
|
|
@@ -177,12 +178,12 @@ def to_database(body: dict[str, Any]) -> Database:
|
|
|
177
178
|
base=_present(body, "base", str),
|
|
178
179
|
name=_present(body, "name", str),
|
|
179
180
|
summary=_present(body, "summary", str),
|
|
180
|
-
standing=_present(body, "standing", str),
|
|
181
|
-
license_type=_nullable(body, "license_type", str),
|
|
182
|
-
starts=_datetime(body["starts"]),
|
|
183
|
-
expires=_datetime(body["expires"]),
|
|
184
|
-
renews_at=_datetime(body["renews_at"]),
|
|
185
|
-
notice_due_at=_datetime(body["notice_due_at"]),
|
|
181
|
+
standing=_one_of(_present(body, "standing", str), STANDINGS, "standing"),
|
|
182
|
+
license_type=_one_of(_nullable(body, "license_type", str), LICENSE_TYPES, "license_type"),
|
|
183
|
+
starts=_datetime(body["starts"], "starts"),
|
|
184
|
+
expires=_datetime(body["expires"], "expires"),
|
|
185
|
+
renews_at=_datetime(body["renews_at"], "renews_at"),
|
|
186
|
+
notice_due_at=_datetime(body["notice_due_at"], "notice_due_at"),
|
|
186
187
|
versions=tuple(to_version(v) for v in _present(body, "versions", list)),
|
|
187
188
|
raw=body,
|
|
188
189
|
open=_present(body, "open", bool),
|
|
@@ -190,26 +191,37 @@ def to_database(body: dict[str, Any]) -> Database:
|
|
|
190
191
|
|
|
191
192
|
|
|
192
193
|
def to_version(body: dict[str, Any]) -> DatabaseVersion:
|
|
194
|
+
sample_formats = _nullable(body, "sample_formats", list, absent=True)
|
|
193
195
|
return DatabaseVersion(
|
|
194
196
|
id=_present(body, "id", str),
|
|
195
197
|
version=_present(body, "version", int),
|
|
196
198
|
summary=_present(body, "summary", str),
|
|
197
199
|
formats=_strings(_present(body, "formats", list), "formats"),
|
|
198
|
-
sample_formats=
|
|
200
|
+
sample_formats=None
|
|
201
|
+
if sample_formats is None
|
|
202
|
+
else _strings(sample_formats, "sample_formats"),
|
|
199
203
|
)
|
|
200
204
|
|
|
201
205
|
|
|
202
206
|
def to_metadata(body: dict[str, Any]) -> DatabaseMetadata:
|
|
207
|
+
schema = _present(body, "schema", dict)
|
|
208
|
+
sample = _of_type(body.get("sample", {}), "sample", dict)
|
|
203
209
|
return DatabaseMetadata(
|
|
204
210
|
id=_present(body, "id", str),
|
|
205
|
-
updated=
|
|
211
|
+
updated=_date(_present(body, "updated", str), "updated"),
|
|
206
212
|
entries=_present(body, "entries", int),
|
|
207
213
|
schema={
|
|
208
|
-
fmt: tuple(
|
|
214
|
+
fmt: tuple(
|
|
215
|
+
to_column(_of_type(c, "schema", dict)) for c in _of_type(cols, "schema", list)
|
|
216
|
+
)
|
|
217
|
+
for fmt, cols in schema.items()
|
|
209
218
|
},
|
|
210
219
|
size=_sizes(_present(body, "size", dict), "size"),
|
|
211
|
-
update_freq=body
|
|
212
|
-
sample={
|
|
220
|
+
update_freq=_nullable(body, "update_freq", str, absent=True),
|
|
221
|
+
sample={
|
|
222
|
+
fmt: tuple(_of_type(row, "sample", dict) for row in _of_type(rows, "sample", list))
|
|
223
|
+
for fmt, rows in sample.items()
|
|
224
|
+
},
|
|
213
225
|
raw=body,
|
|
214
226
|
sample_size=_sizes(body["sample_size"], "sample_size")
|
|
215
227
|
if body.get("sample_size") is not None
|
|
@@ -222,7 +234,7 @@ def to_column(body: dict[str, Any]) -> MetadataColumn:
|
|
|
222
234
|
return MetadataColumn(
|
|
223
235
|
name=_present(body, "name", str),
|
|
224
236
|
type=_present(body, "type", str),
|
|
225
|
-
description=body
|
|
237
|
+
description=_nullable(body, "description", str, absent=True),
|
|
226
238
|
)
|
|
227
239
|
|
|
228
240
|
|
|
@@ -230,13 +242,13 @@ def to_download(body: dict[str, Any]) -> Download:
|
|
|
230
242
|
return Download(
|
|
231
243
|
dataset_id=_present(body, "dataset_id", str),
|
|
232
244
|
format=_present(body, "format", str),
|
|
233
|
-
outcome=_present(body, "outcome", str),
|
|
234
|
-
created=
|
|
245
|
+
outcome=_one_of(_present(body, "outcome", str), get_args(Outcome), "outcome"),
|
|
246
|
+
created=_date_time(_present(body, "created", str), "created"),
|
|
235
247
|
bytes=_nullable(body, "bytes", int),
|
|
236
248
|
http_status=_nullable(body, "http_status", int),
|
|
237
|
-
apikey_id=body
|
|
238
|
-
client_ip=body
|
|
239
|
-
user_agent=body
|
|
249
|
+
apikey_id=_nullable(body, "apikey_id", str),
|
|
250
|
+
client_ip=_nullable(body, "client_ip", str),
|
|
251
|
+
user_agent=_nullable(body, "user_agent", str),
|
|
240
252
|
sample=_present(body, "sample", bool),
|
|
241
253
|
open=_present(body, "open", bool),
|
|
242
254
|
)
|
|
@@ -273,25 +285,74 @@ def _strings(values: list[Any], name: str) -> tuple[Any, ...]:
|
|
|
273
285
|
return tuple(_of_type(v, name, str) for v in values)
|
|
274
286
|
|
|
275
287
|
|
|
276
|
-
def
|
|
277
|
-
|
|
288
|
+
def _one_of(value: Any, allowed: tuple[str, ...], name: str) -> Any:
|
|
289
|
+
"""`value`, a member of `allowed`, or None. One outside it is a value this client was not
|
|
290
|
+
built for: `license_type` came back as served while its type promised one of three, and
|
|
291
|
+
`standing` beside it was refused (2.9.2, measured 2026-10-25)."""
|
|
292
|
+
if value is not None and value not in allowed:
|
|
293
|
+
raise ValueError(f"{name} is not one of {', '.join(allowed)}: {value!r}")
|
|
294
|
+
return value
|
|
278
295
|
|
|
279
296
|
|
|
280
|
-
def
|
|
281
|
-
return
|
|
297
|
+
def _sizes(sizes: Any, name: str) -> dict[str, int]:
|
|
298
|
+
return {fmt: _of_type(n, name, int) for fmt, n in _of_type(sizes, name, dict).items()}
|
|
282
299
|
|
|
283
300
|
|
|
284
|
-
def _datetime(value: Any) -> datetime.datetime | None:
|
|
301
|
+
def _datetime(value: Any, name: str) -> datetime.datetime | None:
|
|
285
302
|
"""A NULLABLE wire timestamp. A license with no end date carries `expires: null`.
|
|
286
303
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
raises, and the caller reports it as the server's failure rather than as None, which
|
|
290
|
-
would quietly read as "no end date".
|
|
304
|
+
A value that is neither null nor readable raises, and the caller reports it as the
|
|
305
|
+
server's failure rather than as None, which would quietly read as "no end date".
|
|
291
306
|
"""
|
|
292
|
-
if value is None
|
|
293
|
-
|
|
294
|
-
|
|
307
|
+
return None if value is None else _date_time(value, name)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# The spec's `date-time` and `date` are RFC 3339's, and only those are read: `fromisoformat`
|
|
311
|
+
# took a date-time with no offset as a naive datetime, which no aware one can be compared
|
|
312
|
+
# with, a date alone as midnight, `+0500`, `+05`, `+05:00:30`, `+05:60` as an hour more,
|
|
313
|
+
# a space for the `T`, no seconds, a comma for the point, week dates and `20261005`
|
|
314
|
+
# (2.9.2, measured 2026-10-25). RFC 3339 allows a lowercase `t` and `z`, which it refused.
|
|
315
|
+
_DIGITS2 = "([0-9]{2})"
|
|
316
|
+
_FULL_DATE = re.compile(rf"([0-9]{{4}})-{_DIGITS2}-{_DIGITS2}")
|
|
317
|
+
_DATE_TIME = re.compile(
|
|
318
|
+
rf"([0-9]{{4}})-{_DIGITS2}-{_DIGITS2}[Tt]{_DIGITS2}:{_DIGITS2}:{_DIGITS2}(?:\.([0-9]+))?"
|
|
319
|
+
rf"(?:[Zz]|([+-]){_DIGITS2}:{_DIGITS2})"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _date(value: str, name: str) -> datetime.date:
|
|
324
|
+
match = _FULL_DATE.fullmatch(value)
|
|
325
|
+
if match is None:
|
|
326
|
+
raise ValueError(f"{name} is not an RFC 3339 date: {value!r}")
|
|
327
|
+
# A day its month lacks, 30 February, raises here.
|
|
328
|
+
return datetime.date(*(int(part) for part in match.groups()))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _date_time(value: Any, name: str) -> datetime.datetime:
|
|
332
|
+
match = _DATE_TIME.fullmatch(_of_type(value, name, str))
|
|
333
|
+
if match is None:
|
|
334
|
+
raise ValueError(f"{name} is not an RFC 3339 date-time: {value!r}")
|
|
335
|
+
year, month, day, hour, minute, second, fraction, sign, offset_hour, offset_minute = (
|
|
336
|
+
match.groups()
|
|
337
|
+
)
|
|
338
|
+
zone = datetime.UTC
|
|
339
|
+
if sign is not None:
|
|
340
|
+
if int(offset_hour) > 23 or int(offset_minute) > 59:
|
|
341
|
+
raise ValueError(f"{name} has no such offset: {value!r}")
|
|
342
|
+
offset = datetime.timedelta(hours=int(offset_hour), minutes=int(offset_minute))
|
|
343
|
+
zone = datetime.timezone(-offset if sign == "-" else offset)
|
|
344
|
+
# A day its month lacks, an hour of 24 and a leap second's 60 raise here; past six
|
|
345
|
+
# digits a fraction is cut, as `fromisoformat` cuts it.
|
|
346
|
+
return datetime.datetime(
|
|
347
|
+
int(year),
|
|
348
|
+
int(month),
|
|
349
|
+
int(day),
|
|
350
|
+
int(hour),
|
|
351
|
+
int(minute),
|
|
352
|
+
int(second),
|
|
353
|
+
int((fraction or "")[:6].ljust(6, "0")),
|
|
354
|
+
tzinfo=zone,
|
|
355
|
+
)
|
|
295
356
|
|
|
296
357
|
|
|
297
358
|
@dataclass(frozen=True, slots=True)
|
|
@@ -454,6 +454,86 @@ UNREADABLE_MEMBERS.update(
|
|
|
454
454
|
}
|
|
455
455
|
)
|
|
456
456
|
|
|
457
|
+
_CHECKSUM = {"id": "bogon_ip_v1", "format": "csvgz", "checksums": DIGESTS}
|
|
458
|
+
|
|
459
|
+
# Each of these came back from 2.9.2 as an answer (measured 2026-10-25): an object where a
|
|
460
|
+
# list is due as an empty one, a member typed nowhere but the spec as served, a
|
|
461
|
+
# `license_type` outside its three, and a checksum answer's unread `id` whatever it held.
|
|
462
|
+
UNREADABLE_MEMBERS.update(
|
|
463
|
+
{
|
|
464
|
+
"list databases as an object": (LIST_PATH, {"databases": {}}),
|
|
465
|
+
"downloads as an object": (DOWNLOADS_PATH, {"downloads": {}}),
|
|
466
|
+
"list license_type outside its enum": (
|
|
467
|
+
LIST_PATH,
|
|
468
|
+
{"databases": [_with("license_type", "trial", database("bogon_ip"))]},
|
|
469
|
+
),
|
|
470
|
+
"list sample_formats as an object": (
|
|
471
|
+
LIST_PATH,
|
|
472
|
+
{"databases": [_version_with("sample_formats", {})]},
|
|
473
|
+
),
|
|
474
|
+
"metadata update_freq as a number": (METADATA_PATH, _with("update_freq", 1, METADATA)),
|
|
475
|
+
"metadata column description as a number": (
|
|
476
|
+
METADATA_PATH,
|
|
477
|
+
{**METADATA, "schema": {"csvgz": [{"name": "ip", "type": "string", "description": 1}]}},
|
|
478
|
+
),
|
|
479
|
+
"metadata columns as an object": (METADATA_PATH, {**METADATA, "schema": {"csvgz": {}}}),
|
|
480
|
+
"metadata sample rows as an object": (METADATA_PATH, {**METADATA, "sample": {"csvgz": {}}}),
|
|
481
|
+
"metadata sample row as pairs": (
|
|
482
|
+
METADATA_PATH,
|
|
483
|
+
{**METADATA, "sample": {"csvgz": [[["first_ip", "10.0.0.0"]]]}},
|
|
484
|
+
),
|
|
485
|
+
**{
|
|
486
|
+
f"downloads {m} as a number": (DOWNLOADS_PATH, {"downloads": [_with(m, 7, _DOWNLOAD)]})
|
|
487
|
+
for m in ("apikey_id", "client_ip", "user_agent")
|
|
488
|
+
},
|
|
489
|
+
"checksums id as null": (CHECKSUM_PATH, _with("id", None, _CHECKSUM)),
|
|
490
|
+
"checksums id as a number": (CHECKSUM_PATH, _with("id", 7, _CHECKSUM)),
|
|
491
|
+
"checksums as pairs": (CHECKSUM_PATH, _with("checksums", list(DIGESTS.items()), _CHECKSUM)),
|
|
492
|
+
}
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# What `fromisoformat` read as a date-time and RFC 3339 does not (2.9.2, measured
|
|
496
|
+
# 2026-10-25): no offset, which made a naive datetime, a date alone, a space for the `T`,
|
|
497
|
+
# offsets without their colon, with seconds or past 23:59, no seconds, a comma for the
|
|
498
|
+
# point, the basic format and a week date. Then what neither reads.
|
|
499
|
+
NOT_DATE_TIMES = [
|
|
500
|
+
"2026-10-05T10:00:00",
|
|
501
|
+
"2026-10-05",
|
|
502
|
+
"2026-10-05 10:00:00Z",
|
|
503
|
+
"2026-10-05T10:00:00+0500",
|
|
504
|
+
"2026-10-05T10:00:00+05",
|
|
505
|
+
"2026-10-05T10:00:00+05:00:30",
|
|
506
|
+
"2026-10-05T10:00:00+05:60",
|
|
507
|
+
"2026-10-05T10:00:00+24:00",
|
|
508
|
+
"2026-10-05T10:00Z",
|
|
509
|
+
"2026-10-05T10:00:00,5Z",
|
|
510
|
+
"20261005T100000Z",
|
|
511
|
+
"2026-W41-1T10:00:00Z",
|
|
512
|
+
"2026-10-5T10:00:00Z",
|
|
513
|
+
" 2026-10-05T10:00:00Z",
|
|
514
|
+
"2026-02-30T10:00:00Z",
|
|
515
|
+
"2026-10-05T23:59:60Z",
|
|
516
|
+
"tomorrow",
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
# The same for `updated`, a `date`: the basic format and week dates were read.
|
|
520
|
+
NOT_DATES = ["20261005", "2026-W41-1", "2026-W41", "2026-10-5", "2026-02-30", "2026-10-05T00:00Z"]
|
|
521
|
+
|
|
522
|
+
for _value in NOT_DATE_TIMES:
|
|
523
|
+
UNREADABLE_MEMBERS[f"list starts {_value!r}"] = (
|
|
524
|
+
LIST_PATH,
|
|
525
|
+
{"databases": [database("bogon_ip", starts=_value)]},
|
|
526
|
+
)
|
|
527
|
+
UNREADABLE_MEMBERS[f"downloads created {_value!r}"] = (
|
|
528
|
+
DOWNLOADS_PATH,
|
|
529
|
+
{"downloads": [_with("created", _value, _DOWNLOAD)]},
|
|
530
|
+
)
|
|
531
|
+
for _value in NOT_DATES:
|
|
532
|
+
UNREADABLE_MEMBERS[f"metadata updated {_value!r}"] = (
|
|
533
|
+
METADATA_PATH,
|
|
534
|
+
_with("updated", _value, METADATA),
|
|
535
|
+
)
|
|
536
|
+
|
|
457
537
|
MEMBER_CALLS = {LIST_PATH: "list", METADATA_PATH: "metadata", DOWNLOADS_PATH: "downloads"}
|
|
458
538
|
|
|
459
539
|
|
|
@@ -475,6 +555,32 @@ def test_an_answer_missing_what_the_spec_requires_is_a_retried_server_error(
|
|
|
475
555
|
assert len(stub.requests) == 3
|
|
476
556
|
|
|
477
557
|
|
|
558
|
+
@pytest.mark.parametrize(
|
|
559
|
+
"value",
|
|
560
|
+
[
|
|
561
|
+
"2026-10-05T10:00:00Z",
|
|
562
|
+
"2026-10-05t10:00:00z",
|
|
563
|
+
"2026-10-05T10:00:00-00:00",
|
|
564
|
+
"2026-10-05T15:30:00+05:30",
|
|
565
|
+
"2026-10-05T05:00:00.123456789-05:00",
|
|
566
|
+
],
|
|
567
|
+
)
|
|
568
|
+
def test_an_rfc_3339_date_time_is_read_as_the_instant_it_names(
|
|
569
|
+
make_client: ClientFactory, value: str
|
|
570
|
+
) -> None:
|
|
571
|
+
"""In each of RFC 3339's spellings, the lowercase ones `fromisoformat` refused included;
|
|
572
|
+
past six digits a fraction is cut."""
|
|
573
|
+
stub = Stub({LIST_PATH: {"body": {"databases": [database("bogon_ip", starts=value)]}}})
|
|
574
|
+
client = make_client(transport=stub.transport)
|
|
575
|
+
|
|
576
|
+
starts = client.database.list()[0].starts
|
|
577
|
+
|
|
578
|
+
assert starts is not None and starts.utcoffset() is not None
|
|
579
|
+
expected = datetime.datetime(2026, 10, 5, 10, tzinfo=datetime.UTC)
|
|
580
|
+
assert starts.replace(microsecond=0) == expected
|
|
581
|
+
assert starts.microsecond == (123456 if "." in value else 0)
|
|
582
|
+
|
|
583
|
+
|
|
478
584
|
@pytest.mark.parametrize(
|
|
479
585
|
("status", "body", "kind"),
|
|
480
586
|
[
|
|
@@ -538,6 +644,51 @@ def test_retries_are_configurable_on_the_client(make_client: ClientFactory) -> N
|
|
|
538
644
|
assert len(stub.requests) == 1
|
|
539
645
|
|
|
540
646
|
|
|
647
|
+
@pytest.mark.parametrize("client", [InternetData, AsyncInternetData])
|
|
648
|
+
@pytest.mark.parametrize(
|
|
649
|
+
"retries", [-1, float("nan"), 1.5, float("-inf"), "2", None, True, 10**400 * -1]
|
|
650
|
+
)
|
|
651
|
+
def test_a_retry_count_that_is_not_a_whole_number_from_0_is_refused_when_the_client_is_built(
|
|
652
|
+
client: type[InternetData | AsyncInternetData], retries: Any
|
|
653
|
+
) -> None:
|
|
654
|
+
"""Accepted by 2.9.2: NaN retried a failing call until it succeeded, -1 read as 0, 1.5 as
|
|
655
|
+
two retries, and a string or None raised a raw TypeError out of every failing call."""
|
|
656
|
+
with pytest.raises(ValueError, match="retries"):
|
|
657
|
+
client(API_KEY, retries=retries)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@pytest.mark.parametrize("retries", [0, 2, 2.0])
|
|
661
|
+
def test_a_whole_retry_count_is_what_a_failing_call_spends(
|
|
662
|
+
make_client: ClientFactory, monkeypatch: pytest.MonkeyPatch, retries: float
|
|
663
|
+
) -> None:
|
|
664
|
+
monkeypatch.setattr(_core, "_BACKOFF_BASE", 0.0)
|
|
665
|
+
stub = Stub({LIST_PATH: {"status": 503, "body": {"rc": "UNAVAILABLE"}}})
|
|
666
|
+
client = make_client(transport=stub.transport, retries=retries)
|
|
667
|
+
|
|
668
|
+
with pytest.raises(InternetDataError):
|
|
669
|
+
client.database.list()
|
|
670
|
+
|
|
671
|
+
assert len(stub.requests) == retries + 1
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def test_an_infinite_retry_count_retries_until_the_call_succeeds(
|
|
675
|
+
make_client: ClientFactory, monkeypatch: pytest.MonkeyPatch
|
|
676
|
+
) -> None:
|
|
677
|
+
monkeypatch.setattr(_core, "_BACKOFF_BASE", 0.0)
|
|
678
|
+
sent: list[httpx.Request] = []
|
|
679
|
+
|
|
680
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
681
|
+
sent.append(request)
|
|
682
|
+
if len(sent) <= 6:
|
|
683
|
+
return httpx.Response(503, json={"rc": "UNAVAILABLE"})
|
|
684
|
+
return httpx.Response(200, json={"databases": []})
|
|
685
|
+
|
|
686
|
+
client = make_client(transport=httpx.MockTransport(handler), retries=float("inf"))
|
|
687
|
+
|
|
688
|
+
assert client.database.list() == []
|
|
689
|
+
assert len(sent) == 7
|
|
690
|
+
|
|
691
|
+
|
|
541
692
|
def test_a_spent_quota_is_never_retried(make_client: ClientFactory) -> None:
|
|
542
693
|
stub = Stub({LIST_PATH: {"status": 429, "body": {"rc": "QUOTA_EXCEEDED"}}})
|
|
543
694
|
client = make_client(transport=stub.transport, retries=3)
|
|
@@ -745,7 +896,7 @@ def test_a_per_call_timeout_bounds_a_trickling_body_and_leaves_the_clients_own_a
|
|
|
745
896
|
_assert_bounded_by(default, CLIENT_TIMEOUT)
|
|
746
897
|
|
|
747
898
|
|
|
748
|
-
@pytest.mark.parametrize("timeout", [0, -1, float("nan"), float("inf"), 1e300, "30", True])
|
|
899
|
+
@pytest.mark.parametrize("timeout", [0, -1, float("nan"), float("inf"), 1e300, 10**400, "30", True])
|
|
749
900
|
@pytest.mark.parametrize("call", PER_CALL)
|
|
750
901
|
def test_a_per_call_timeout_no_attempt_can_meet_is_refused_before_any_request(
|
|
751
902
|
make_client: ClientFactory, call: str, timeout: Any
|
|
@@ -799,7 +950,7 @@ def test_the_default_timeout_is_thirty_seconds() -> None:
|
|
|
799
950
|
|
|
800
951
|
@pytest.mark.parametrize("client", [InternetData, AsyncInternetData])
|
|
801
952
|
@pytest.mark.parametrize(
|
|
802
|
-
"timeout", [0, -1, 0.0, float("nan"), float("inf"), 9223372037, 1e300, "30", True]
|
|
953
|
+
"timeout", [0, -1, 0.0, float("nan"), float("inf"), 9223372037, 1e300, 10**400, "30", True]
|
|
803
954
|
)
|
|
804
955
|
def test_a_timeout_no_attempt_can_meet_is_refused_when_the_client_is_built(
|
|
805
956
|
client: type[InternetData | AsyncInternetData], timeout: Any
|
|
@@ -168,7 +168,7 @@ def test_the_listing_is_returned_exactly_as_served(make_client: ClientFactory) -
|
|
|
168
168
|
def test_no_catalog_is_compiled_into_the_client(make_client: ClientFactory) -> None:
|
|
169
169
|
"""An empty listing must stay empty.
|
|
170
170
|
|
|
171
|
-
This is the mistake worth guarding: a client that knows the published database
|
|
171
|
+
This is the mistake worth guarding: a client that knows the published database IDs -
|
|
172
172
|
from a bundled table, an enum, or a merge with anything - would hand back a family the
|
|
173
173
|
server deliberately withheld, and the leak answers 200 while moving no bytes.
|
|
174
174
|
"""
|
|
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.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/account/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/api/api_keys/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/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
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/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
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/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.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_org_ref.py
RENAMED
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_org_wrap.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/account_user.py
RENAMED
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/apikey_detail.py
RENAMED
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/client_registration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/database_format.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/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.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/db_checksums.py
RENAMED
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/device_authorization.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/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.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/oauth_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/revoke_request.py
RENAMED
|
File without changes
|
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/src/internetdata/_generated/models/token_request.py
RENAMED
|
File without changes
|
{internetdata-2.9.2 → internetdata-2.9.4}/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
|