internetdata 2.1.0__tar.gz → 2.2.0__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.1.0 → internetdata-2.2.0}/PKG-INFO +8 -4
- {internetdata-2.1.0 → internetdata-2.2.0}/README.md +7 -3
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/__init__.py +7 -1
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_core.py +25 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/aio.py +2 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/client.py +6 -3
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/models.py +32 -5
- {internetdata-2.1.0 → internetdata-2.2.0}/testdata/testdata.json +15 -1
- {internetdata-2.1.0 → internetdata-2.2.0}/tests/test_client.py +79 -1
- {internetdata-2.1.0 → internetdata-2.2.0}/tests/test_conformance.py +36 -14
- {internetdata-2.1.0 → internetdata-2.2.0}/.gitignore +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/LICENSE +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/pyproject.toml +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/scripts/download-spec.sh +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/scripts/generate.sh +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/scripts/publish.sh +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/scripts/v2_subset.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/spec/openapi.yaml +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/__init__.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/api/__init__.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/api/database_v_2/__init__.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/api/database_v_2/database_checksum_v2.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/api/database_v_2/database_metadata_v2.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/api/database_v_2/download_database_v2.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/api/database_v_2/list_databases.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/api/database_v_2/list_downloads.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/client.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/errors.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/__init__.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_checksum_v2_response_200.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_format.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_license_type_type_1.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_license_type_type_2_type_1.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_license_type_type_3_type_1.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_metadata.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_metadata_column.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_metadata_sample.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_metadata_sample_additional_property_item.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_metadata_schema.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_metadata_size.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_version.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/db_checksums.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/download.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/download_outcome.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/error.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/list_databases_response_200.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/list_downloads_response_200.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/standing.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/types.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/errors.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/py.typed +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/tests/conftest.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/tests/helpers.py +0 -0
- {internetdata-2.1.0 → internetdata-2.2.0}/tests/test_download.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: internetdata
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
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
|
|
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():
|
|
@@ -82,7 +82,9 @@ for family in client.database.list():
|
|
|
82
82
|
print(version.id, version.formats) # 'bogon_ip_v1' ('csvgz', 'mmdb')
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
`standing` is `licensed`, `expired` or `unlicensed`, and `license_type` is what your
|
|
85
|
+
`standing` is `licensed`, `expired` or `unlicensed`, and `license_type` is what your license lets you do with the data (`evaluation`, `standard`, `redistribute`, or `None` when there is no license). A family you have never bought is still listed, as `unlicensed`, so you can see what else exists.
|
|
86
|
+
|
|
87
|
+
A rolling license also carries `renews_at`, when it next renews, and `notice_due_at`, the last day you can give notice of non-renewal for that term. Both are `None` when there is no license, when it has no defined term, or when `expires` sets a hard stop instead. `DATABASE_FORMATS`, `STANDINGS` and `LICENSE_TYPES` hold the published values at runtime, for checking one that came from a flag or a form before you make a call.
|
|
86
88
|
|
|
87
89
|
### What is inside a build
|
|
88
90
|
|
|
@@ -165,7 +167,7 @@ except InternetDataError as err:
|
|
|
165
167
|
|
|
166
168
|
`kind` is one of `bad_request`, `unauthorized`, `forbidden`, `rate_limited`, `quota_exceeded`, `server_error` or `network`. `message` is the API's own result code, passed through as it was sent, so you can switch on `NOT_LICENSED` against `LICENSE_EXPIRED` without reading the status.
|
|
167
169
|
|
|
168
|
-
A request that runs past its `timeout` fails with `network`, and is retried like any other network failure. The default is 30 seconds per attempt, body included, so a retried call can take longer in total. A database transfer is exempt, so `download` and `download_bytes` are never cut off part way through a large file. Set it on the client, in seconds, or pass `None` for no bound:
|
|
170
|
+
A request that runs past its `timeout` fails with `network`, and is retried like any other network failure. The default is 30 seconds per attempt, body included, so a retried call can take longer in total. A database transfer is exempt, so `download` and `download_bytes` are never cut off part way through a large file. Set it on the client, in seconds, or pass `None` for no bound. Anything else that is not a number greater than 0 raises `ValueError` when the client is built:
|
|
169
171
|
|
|
170
172
|
```python
|
|
171
173
|
client = InternetData(api_key, timeout=10)
|
|
@@ -173,6 +175,8 @@ client = InternetData(api_key, timeout=10)
|
|
|
173
175
|
|
|
174
176
|
**Changed in 2.1.0:** the default was 10 seconds, and it bounded each read of a response rather than the whole attempt, so a response trickling in slowly could run past it for as long as the server kept sending.
|
|
175
177
|
|
|
178
|
+
**Changed in 2.2.0:** a timeout of 0 or less, NaN or a string used to be accepted, and failed every call.
|
|
179
|
+
|
|
176
180
|
Note that `rate_limited` and `quota_exceeded` both arrive as HTTP 429 and are not the same thing. A rate limit is the API facing a traffic burst, and retrying later works; a spent quota needs your allowance raised or the window to roll over. The library retries rate limits for you, and server and network failures, but never a spent quota or anything else you sent.
|
|
177
181
|
|
|
178
182
|
## Other Libraries
|
|
@@ -38,7 +38,7 @@ with InternetData(api_key) as client:
|
|
|
38
38
|
|
|
39
39
|
### The catalog
|
|
40
40
|
|
|
41
|
-
`list` answers database FAMILIES. A
|
|
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():
|
|
@@ -48,7 +48,9 @@ for family in client.database.list():
|
|
|
48
48
|
print(version.id, version.formats) # 'bogon_ip_v1' ('csvgz', 'mmdb')
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
`standing` is `licensed`, `expired` or `unlicensed`, and `license_type` is what your
|
|
51
|
+
`standing` is `licensed`, `expired` or `unlicensed`, and `license_type` is what your license lets you do with the data (`evaluation`, `standard`, `redistribute`, or `None` when there is no license). A family you have never bought is still listed, as `unlicensed`, so you can see what else exists.
|
|
52
|
+
|
|
53
|
+
A rolling license also carries `renews_at`, when it next renews, and `notice_due_at`, the last day you can give notice of non-renewal for that term. Both are `None` when there is no license, when it has no defined term, or when `expires` sets a hard stop instead. `DATABASE_FORMATS`, `STANDINGS` and `LICENSE_TYPES` hold the published values at runtime, for checking one that came from a flag or a form before you make a call.
|
|
52
54
|
|
|
53
55
|
### What is inside a build
|
|
54
56
|
|
|
@@ -131,7 +133,7 @@ except InternetDataError as err:
|
|
|
131
133
|
|
|
132
134
|
`kind` is one of `bad_request`, `unauthorized`, `forbidden`, `rate_limited`, `quota_exceeded`, `server_error` or `network`. `message` is the API's own result code, passed through as it was sent, so you can switch on `NOT_LICENSED` against `LICENSE_EXPIRED` without reading the status.
|
|
133
135
|
|
|
134
|
-
A request that runs past its `timeout` fails with `network`, and is retried like any other network failure. The default is 30 seconds per attempt, body included, so a retried call can take longer in total. A database transfer is exempt, so `download` and `download_bytes` are never cut off part way through a large file. Set it on the client, in seconds, or pass `None` for no bound:
|
|
136
|
+
A request that runs past its `timeout` fails with `network`, and is retried like any other network failure. The default is 30 seconds per attempt, body included, so a retried call can take longer in total. A database transfer is exempt, so `download` and `download_bytes` are never cut off part way through a large file. Set it on the client, in seconds, or pass `None` for no bound. Anything else that is not a number greater than 0 raises `ValueError` when the client is built:
|
|
135
137
|
|
|
136
138
|
```python
|
|
137
139
|
client = InternetData(api_key, timeout=10)
|
|
@@ -139,6 +141,8 @@ client = InternetData(api_key, timeout=10)
|
|
|
139
141
|
|
|
140
142
|
**Changed in 2.1.0:** the default was 10 seconds, and it bounded each read of a response rather than the whole attempt, so a response trickling in slowly could run past it for as long as the server kept sending.
|
|
141
143
|
|
|
144
|
+
**Changed in 2.2.0:** a timeout of 0 or less, NaN or a string used to be accepted, and failed every call.
|
|
145
|
+
|
|
142
146
|
Note that `rate_limited` and `quota_exceeded` both arrive as HTTP 429 and are not the same thing. A rate limit is the API facing a traffic burst, and retrying later works; a spent quota needs your allowance raised or the window to roll over. The library retries rate limits for you, and server and network failures, but never a spent quota or anything else you sent.
|
|
143
147
|
|
|
144
148
|
## Other Libraries
|
|
@@ -15,6 +15,9 @@ from .aio import AsyncDatabaseApi, AsyncInternetData
|
|
|
15
15
|
from .client import DatabaseApi, InternetData
|
|
16
16
|
from .errors import ErrorKind, InternetDataError
|
|
17
17
|
from .models import (
|
|
18
|
+
DATABASE_FORMATS,
|
|
19
|
+
LICENSE_TYPES,
|
|
20
|
+
STANDINGS,
|
|
18
21
|
Database,
|
|
19
22
|
DatabaseMetadata,
|
|
20
23
|
DatabaseVersion,
|
|
@@ -26,10 +29,13 @@ from .models import (
|
|
|
26
29
|
Standing,
|
|
27
30
|
)
|
|
28
31
|
|
|
29
|
-
__version__ = "2.
|
|
32
|
+
__version__ = "2.2.0"
|
|
30
33
|
|
|
31
34
|
__all__ = [
|
|
35
|
+
"DATABASE_FORMATS",
|
|
32
36
|
"DEFAULT_BASE_URL",
|
|
37
|
+
"LICENSE_TYPES",
|
|
38
|
+
"STANDINGS",
|
|
33
39
|
"AsyncDatabaseApi",
|
|
34
40
|
"AsyncInternetData",
|
|
35
41
|
"Database",
|
|
@@ -7,6 +7,7 @@ import asyncio
|
|
|
7
7
|
import contextlib
|
|
8
8
|
import contextvars
|
|
9
9
|
import json
|
|
10
|
+
import math
|
|
10
11
|
import os
|
|
11
12
|
import threading
|
|
12
13
|
from collections.abc import Awaitable, Callable, Iterator
|
|
@@ -105,6 +106,30 @@ def build_async_transfer_client(
|
|
|
105
106
|
)
|
|
106
107
|
|
|
107
108
|
|
|
109
|
+
def check_timeout(timeout: float | None) -> float | None:
|
|
110
|
+
"""`timeout`, once it is a bound an attempt can meet.
|
|
111
|
+
|
|
112
|
+
Refused where it is SET, because nothing downstream refuses it: zero, a negative
|
|
113
|
+
number, NaN or a string reached the first call and failed it, and every call after,
|
|
114
|
+
as a retried `network` error after three seconds of backoff, or for a string as a
|
|
115
|
+
`server_error` blaming the API. Infinity is refused too, since the sync client's wait
|
|
116
|
+
cannot hold it (`OverflowError`); None is the spelling for no bound.
|
|
117
|
+
"""
|
|
118
|
+
if timeout is None:
|
|
119
|
+
return None
|
|
120
|
+
if (
|
|
121
|
+
isinstance(timeout, bool)
|
|
122
|
+
or not isinstance(timeout, int | float)
|
|
123
|
+
or not math.isfinite(timeout)
|
|
124
|
+
or timeout <= 0
|
|
125
|
+
):
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"timeout must be a number of seconds greater than 0, or None for no bound, "
|
|
128
|
+
f"not {timeout!r}"
|
|
129
|
+
)
|
|
130
|
+
return timeout
|
|
131
|
+
|
|
132
|
+
|
|
108
133
|
def storage_refusal(res: httpx.Response) -> InternetDataError:
|
|
109
134
|
"""What object storage refusing a download link becomes.
|
|
110
135
|
|
|
@@ -26,6 +26,7 @@ from ._core import (
|
|
|
26
26
|
assert_whole_transfer,
|
|
27
27
|
build_async_transfer_client,
|
|
28
28
|
build_client,
|
|
29
|
+
check_timeout,
|
|
29
30
|
checksums_of,
|
|
30
31
|
databases_of,
|
|
31
32
|
downloads_of,
|
|
@@ -73,6 +74,7 @@ class AsyncInternetData:
|
|
|
73
74
|
timeout: float | None = DEFAULT_TIMEOUT,
|
|
74
75
|
transport: httpx.AsyncBaseTransport | None = None,
|
|
75
76
|
) -> None:
|
|
77
|
+
timeout = check_timeout(timeout)
|
|
76
78
|
self._client = build_client(api_key, base_url, timeout, transport)
|
|
77
79
|
self._transfer = build_async_transfer_client(timeout, transport)
|
|
78
80
|
self._retries = retries
|
|
@@ -21,6 +21,7 @@ from ._core import (
|
|
|
21
21
|
assert_whole_transfer,
|
|
22
22
|
build_client,
|
|
23
23
|
build_transfer_client,
|
|
24
|
+
check_timeout,
|
|
24
25
|
checksums_of,
|
|
25
26
|
databases_of,
|
|
26
27
|
downloads_of,
|
|
@@ -55,12 +56,13 @@ class InternetData:
|
|
|
55
56
|
Every database published today is licensed, so create a key carrying the
|
|
56
57
|
`db.download` scope in the console and pass it in. The argument is optional
|
|
57
58
|
nonetheless, and an absent or empty one sends no `Authorization` header at all
|
|
58
|
-
rather than an empty one: what this API serves without a
|
|
59
|
+
rather than an empty one: what this API serves without a license is a product
|
|
59
60
|
decision, not the client's to refuse.
|
|
60
61
|
|
|
61
62
|
`timeout` is how long one attempt at a request may take, in seconds, body included, so a
|
|
62
63
|
call that is retried can take longer in total; None means no bound, and a database
|
|
63
|
-
transfer is exempt.
|
|
64
|
+
transfer is exempt. Anything else that is not a finite number greater than 0 is a
|
|
65
|
+
`ValueError` here, rather than a failure of every call.
|
|
64
66
|
|
|
65
67
|
Holds an HTTP connection pool, so use it as a context manager or call `close()` when
|
|
66
68
|
you are done with it.
|
|
@@ -78,6 +80,7 @@ class InternetData:
|
|
|
78
80
|
timeout: float | None = DEFAULT_TIMEOUT,
|
|
79
81
|
transport: httpx.BaseTransport | None = None,
|
|
80
82
|
) -> None:
|
|
83
|
+
timeout = check_timeout(timeout)
|
|
81
84
|
self._client = build_client(api_key, base_url, timeout, transport)
|
|
82
85
|
self._transfer = build_transfer_client(timeout, transport)
|
|
83
86
|
self._retries = retries
|
|
@@ -133,7 +136,7 @@ class DatabaseApi:
|
|
|
133
136
|
customer. Nothing is cached and nothing is reconstructed here - what you get is
|
|
134
137
|
what the server sent for the key you are holding.
|
|
135
138
|
|
|
136
|
-
A
|
|
139
|
+
A license covers a FAMILY, while a download names a version, so the ids for the
|
|
137
140
|
other calls come from each entry's `versions`.
|
|
138
141
|
"""
|
|
139
142
|
|
|
@@ -11,9 +11,12 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
|
-
from typing import Any, Literal
|
|
14
|
+
from typing import Any, Literal, get_args
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
|
+
"DATABASE_FORMATS",
|
|
18
|
+
"LICENSE_TYPES",
|
|
19
|
+
"STANDINGS",
|
|
17
20
|
"Database",
|
|
18
21
|
"DatabaseMetadata",
|
|
19
22
|
"DatabaseVersion",
|
|
@@ -37,7 +40,20 @@ Standing = Literal["licensed", "expired", "unlicensed"]
|
|
|
37
40
|
"""Where your organization stands with one database family."""
|
|
38
41
|
|
|
39
42
|
LicenseType = Literal["evaluation", "standard", "redistribute"]
|
|
40
|
-
"""What a
|
|
43
|
+
"""What a license permits you to do with the data. `None` when there is no license."""
|
|
44
|
+
|
|
45
|
+
DATABASE_FORMATS: tuple[Format, ...] = get_args(Format)
|
|
46
|
+
"""Every `Format`, at runtime.
|
|
47
|
+
|
|
48
|
+
A `Literal` is erased to nothing a program can check against, so a format read from a
|
|
49
|
+
flag, a form or a config file has these to be tested against before a call.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
STANDINGS: tuple[Standing, ...] = get_args(Standing)
|
|
53
|
+
"""Every `Standing`, at runtime."""
|
|
54
|
+
|
|
55
|
+
LICENSE_TYPES: tuple[LicenseType, ...] = get_args(LicenseType)
|
|
56
|
+
"""Every `LicenseType`, at runtime. `None`, for no license, is not one of them."""
|
|
41
57
|
|
|
42
58
|
Outcome = Literal["ok", "unauthorized", "denied", "expired", "unknown", "unavailable"]
|
|
43
59
|
"""How one download attempt ended, refusals included."""
|
|
@@ -49,7 +65,7 @@ class DatabaseVersion:
|
|
|
49
65
|
|
|
50
66
|
Old versions are frozen rather than migrated, so several stay downloadable at once.
|
|
51
67
|
`id` is what `download`, `checksums` and `metadata` take; the family `base` is what a
|
|
52
|
-
|
|
68
|
+
license is held against.
|
|
53
69
|
"""
|
|
54
70
|
|
|
55
71
|
id: str
|
|
@@ -60,13 +76,18 @@ class DatabaseVersion:
|
|
|
60
76
|
|
|
61
77
|
@dataclass(frozen=True, slots=True)
|
|
62
78
|
class Database:
|
|
63
|
-
"""One database FAMILY, with your organization's
|
|
79
|
+
"""One database FAMILY, with your organization's license beside it.
|
|
64
80
|
|
|
65
81
|
A family your organization has never licensed is still listed, with `standing` set to
|
|
66
82
|
`unlicensed`, so you can see what else exists. A family commissioned for a single
|
|
67
83
|
customer is a different matter: it is absent from this listing entirely for everyone
|
|
68
84
|
who does not license it. Absence here means "not yours to see", never "does not
|
|
69
85
|
exist", so the catalog is not the same document for every key.
|
|
86
|
+
|
|
87
|
+
A rolling license carries `renews_at`, when it next renews, and `notice_due_at`, the
|
|
88
|
+
last day notice of non-renewal can be given for the term ending then. Both are None
|
|
89
|
+
when there is no license, when it has no defined term, or when `expires` sets a hard
|
|
90
|
+
stop instead; `notice_due_at` is None too when the agreement records no notice period.
|
|
70
91
|
"""
|
|
71
92
|
|
|
72
93
|
base: str
|
|
@@ -76,6 +97,10 @@ class Database:
|
|
|
76
97
|
license_type: LicenseType | None
|
|
77
98
|
starts: datetime.datetime | None
|
|
78
99
|
expires: datetime.datetime | None
|
|
100
|
+
# Keyword-only with a default, so a Database built by hand before 2.2.0 still builds;
|
|
101
|
+
# the parser always sets both.
|
|
102
|
+
renews_at: datetime.datetime | None = field(default=None, kw_only=True)
|
|
103
|
+
notice_due_at: datetime.datetime | None = field(default=None, kw_only=True)
|
|
79
104
|
versions: tuple[DatabaseVersion, ...]
|
|
80
105
|
raw: dict[str, Any] = field(default_factory=dict)
|
|
81
106
|
|
|
@@ -137,6 +162,8 @@ def to_database(body: dict[str, Any]) -> Database:
|
|
|
137
162
|
license_type=body["license_type"],
|
|
138
163
|
starts=_datetime(body["starts"]),
|
|
139
164
|
expires=_datetime(body["expires"]),
|
|
165
|
+
renews_at=_datetime(body["renews_at"]),
|
|
166
|
+
notice_due_at=_datetime(body["notice_due_at"]),
|
|
140
167
|
versions=tuple(to_version(v) for v in body["versions"]),
|
|
141
168
|
raw=body,
|
|
142
169
|
)
|
|
@@ -185,7 +212,7 @@ def to_download(body: dict[str, Any]) -> Download:
|
|
|
185
212
|
|
|
186
213
|
|
|
187
214
|
def _datetime(value: Any) -> datetime.datetime | None:
|
|
188
|
-
"""A NULLABLE wire timestamp. A
|
|
215
|
+
"""A NULLABLE wire timestamp. A license with no end date carries `expires: null`.
|
|
189
216
|
|
|
190
217
|
`fromisoformat` only learned to read a trailing `Z` in 3.11, which is one of the
|
|
191
218
|
three reasons this package floors there. A value that is neither null nor readable
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
"name": "
|
|
30
|
+
"name": "license-lapsed",
|
|
31
31
|
"status": 403,
|
|
32
32
|
"headers": {},
|
|
33
33
|
"body": {
|
|
@@ -78,6 +78,20 @@
|
|
|
78
78
|
"message": "INVALID_FORMAT"
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
+
{
|
|
82
|
+
"name": "unlisted-4xx-is-not-retryable",
|
|
83
|
+
"why": "The RANGE is the rule, not a list of statuses. An SDK that enumerated 400 and 404 passed every other case here while a 422 fell through to the retryable server_error default.",
|
|
84
|
+
"status": 422,
|
|
85
|
+
"headers": {},
|
|
86
|
+
"body": {
|
|
87
|
+
"rc": "INVALID_REQUEST"
|
|
88
|
+
},
|
|
89
|
+
"expect": {
|
|
90
|
+
"kind": "bad_request",
|
|
91
|
+
"retryable": false,
|
|
92
|
+
"message": "INVALID_REQUEST"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
81
95
|
{
|
|
82
96
|
"name": "rate-limited-transient",
|
|
83
97
|
"status": 429,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import dataclasses
|
|
6
7
|
import datetime
|
|
7
8
|
import inspect
|
|
@@ -9,6 +10,7 @@ import time
|
|
|
9
10
|
from collections.abc import Callable
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
13
|
+
import attrs
|
|
12
14
|
import httpx
|
|
13
15
|
import pytest
|
|
14
16
|
from helpers import (
|
|
@@ -26,7 +28,26 @@ from helpers import (
|
|
|
26
28
|
settle,
|
|
27
29
|
)
|
|
28
30
|
|
|
29
|
-
from internetdata import
|
|
31
|
+
from internetdata import (
|
|
32
|
+
AsyncInternetData,
|
|
33
|
+
Database,
|
|
34
|
+
DatabaseMetadata,
|
|
35
|
+
DatabaseVersion,
|
|
36
|
+
Download,
|
|
37
|
+
InternetData,
|
|
38
|
+
InternetDataError,
|
|
39
|
+
MetadataColumn,
|
|
40
|
+
_core,
|
|
41
|
+
)
|
|
42
|
+
from internetdata._generated.models.database import Database as WireDatabase
|
|
43
|
+
from internetdata._generated.models.database_metadata import (
|
|
44
|
+
DatabaseMetadata as WireDatabaseMetadata,
|
|
45
|
+
)
|
|
46
|
+
from internetdata._generated.models.database_metadata_column import (
|
|
47
|
+
DatabaseMetadataColumn as WireDatabaseMetadataColumn,
|
|
48
|
+
)
|
|
49
|
+
from internetdata._generated.models.database_version import DatabaseVersion as WireDatabaseVersion
|
|
50
|
+
from internetdata._generated.models.download import Download as WireDownload
|
|
30
51
|
|
|
31
52
|
METADATA = {
|
|
32
53
|
"id": "bogon_ip_v1",
|
|
@@ -141,6 +162,47 @@ def test_a_licence_with_no_end_date_reads_as_none(make_client: ClientFactory) ->
|
|
|
141
162
|
|
|
142
163
|
assert family.starts == datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
|
|
143
164
|
assert family.expires is None
|
|
165
|
+
assert family.renews_at is None
|
|
166
|
+
assert family.notice_due_at is None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_a_rolling_license_carries_its_renewal_dates(make_client: ClientFactory) -> None:
|
|
170
|
+
served = database(
|
|
171
|
+
"bogon_ip",
|
|
172
|
+
renews_at="2027-01-01T00:00:00.000Z",
|
|
173
|
+
notice_due_at="2026-10-02T00:00:00.000Z",
|
|
174
|
+
)
|
|
175
|
+
stub = Stub({LIST_PATH: {"body": {"databases": [served]}}})
|
|
176
|
+
client = make_client(transport=stub.transport)
|
|
177
|
+
|
|
178
|
+
family = client.database.list()[0]
|
|
179
|
+
|
|
180
|
+
assert family.renews_at == datetime.datetime(2027, 1, 1, tzinfo=datetime.UTC)
|
|
181
|
+
assert family.notice_due_at == datetime.datetime(2026, 10, 2, tzinfo=datetime.UTC)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@pytest.mark.parametrize(
|
|
185
|
+
("wire", "ours"),
|
|
186
|
+
[
|
|
187
|
+
(WireDatabase, Database),
|
|
188
|
+
(WireDatabaseVersion, DatabaseVersion),
|
|
189
|
+
(WireDatabaseMetadata, DatabaseMetadata),
|
|
190
|
+
(WireDatabaseMetadataColumn, MetadataColumn),
|
|
191
|
+
(WireDownload, Download),
|
|
192
|
+
],
|
|
193
|
+
)
|
|
194
|
+
def test_every_field_the_pinned_spec_serves_is_on_the_model(wire: Any, ours: Any) -> None:
|
|
195
|
+
"""The staleness pin on the hand-written models.
|
|
196
|
+
|
|
197
|
+
They parse the keys they name and nothing else, so a field a re-pin adds reaches the
|
|
198
|
+
generated code and `raw` while the typed model never hears of it: `renews_at` and
|
|
199
|
+
`notice_due_at` sat that way from the 2026-09-09 re-pin until 2.2.0. The generated
|
|
200
|
+
classes come from the same pinned spec, which makes them the list to check against.
|
|
201
|
+
The generator suffixes a name that shadows a builtin with `_`.
|
|
202
|
+
"""
|
|
203
|
+
served = {f.name.rstrip("_") for f in attrs.fields(wire)} - {"additional_properties"}
|
|
204
|
+
modeled = {f.name for f in dataclasses.fields(ours)} - {"raw"}
|
|
205
|
+
assert served - modeled == set(), f"{ours.__name__} lacks what the spec serves"
|
|
144
206
|
|
|
145
207
|
|
|
146
208
|
def test_a_database_cannot_be_mutated(make_client: ClientFactory) -> None:
|
|
@@ -292,6 +354,22 @@ def test_the_default_timeout_is_thirty_seconds() -> None:
|
|
|
292
354
|
assert inspect.signature(client).parameters["timeout"].default == 30
|
|
293
355
|
|
|
294
356
|
|
|
357
|
+
@pytest.mark.parametrize("client", [InternetData, AsyncInternetData])
|
|
358
|
+
@pytest.mark.parametrize("timeout", [0, -1, 0.0, float("nan"), float("inf"), "30", True])
|
|
359
|
+
def test_a_timeout_no_attempt_can_meet_is_refused_when_the_client_is_built(
|
|
360
|
+
client: type[InternetData | AsyncInternetData], timeout: Any
|
|
361
|
+
) -> None:
|
|
362
|
+
"""Accepted, each of these failed every call instead, after the retries' backoff."""
|
|
363
|
+
with pytest.raises(ValueError, match="timeout"):
|
|
364
|
+
client(API_KEY, timeout=timeout)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@pytest.mark.parametrize("timeout", [None, 0.25, 1, 30])
|
|
368
|
+
def test_a_usable_timeout_builds_a_client(timeout: float | None) -> None:
|
|
369
|
+
InternetData(API_KEY, timeout=timeout).close()
|
|
370
|
+
asyncio.run(AsyncInternetData(API_KEY, timeout=timeout).aclose())
|
|
371
|
+
|
|
372
|
+
|
|
295
373
|
# Refused before the network, and never retried: a typo is the caller's, not a failure of
|
|
296
374
|
# the server's to wait out.
|
|
297
375
|
def test_an_unpublished_format_is_refused_before_any_request(make_client: ClientFactory) -> None:
|
|
@@ -19,7 +19,7 @@ from helpers import (
|
|
|
19
19
|
database,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
-
from internetdata import InternetDataError
|
|
22
|
+
from internetdata import DATABASE_FORMATS, LICENSE_TYPES, STANDINGS, InternetDataError
|
|
23
23
|
|
|
24
24
|
# One live grant, one that has run out, and one never bought. Between them these cover
|
|
25
25
|
# every value the corpus pins for `standing` and `license_type`.
|
|
@@ -65,21 +65,32 @@ def test_every_error_shape_maps_to_the_same_kind_in_every_language(
|
|
|
65
65
|
assert err.retry_after_seconds == expect["retryAfterSeconds"], name
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
# The one that has caught three of four bindings:
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
# The one that has caught three of four bindings: every 4xx but a 429 carrying Retry-After
|
|
69
|
+
# is the CALLER's, and classifying by an enumerated list lets the rest (the corpus's 422)
|
|
70
|
+
# fall through to a retryable server_error. Asserted with retries ON and by counting
|
|
71
|
+
# requests: with retries off, one request is guaranteed and a wrong classifier passes.
|
|
72
|
+
def test_no_client_error_is_ever_retried(make_client: ClientFactory) -> None:
|
|
73
|
+
cases = [case for case in TESTDATA["errors"] if not case["expect"]["retryable"]]
|
|
74
|
+
assert any(case["name"] == "unlisted-4xx-is-not-retryable" for case in cases)
|
|
75
|
+
|
|
76
|
+
for case in cases:
|
|
77
|
+
stub = Stub(
|
|
78
|
+
{
|
|
79
|
+
METADATA_PATH: {
|
|
80
|
+
"status": case["status"],
|
|
81
|
+
"body": case["body"],
|
|
82
|
+
"headers": case["headers"],
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
)
|
|
76
86
|
client = make_client(transport=stub.transport, retries=3)
|
|
77
87
|
|
|
78
88
|
with pytest.raises(InternetDataError) as caught:
|
|
79
89
|
client.database.metadata("nope_v1")
|
|
80
90
|
|
|
81
|
-
|
|
91
|
+
# The count first: a retried case also ends on an error, just a later one.
|
|
82
92
|
assert len(stub.requests) == 1, f"{case['name']}: issued {len(stub.requests)} requests"
|
|
93
|
+
assert caught.value.kind == case["expect"]["kind"], case["name"]
|
|
83
94
|
|
|
84
95
|
|
|
85
96
|
# Both arrive as 429 and the header is the only thing separating them, so a client that
|
|
@@ -118,7 +129,7 @@ def test_the_catalog_carries_every_standing_and_license_type_the_corpus_pins(
|
|
|
118
129
|
assert served_rights <= set(TESTDATA["license_type"]), (
|
|
119
130
|
f"undocumented license_type right in {served_rights}"
|
|
120
131
|
)
|
|
121
|
-
# Null when there is no
|
|
132
|
+
# Null when there is no license, which is a different answer from any of the three.
|
|
122
133
|
assert [f.license_type for f in families][-1] is None
|
|
123
134
|
for family in families:
|
|
124
135
|
assert family.versions, f"{family.base} carries no versions"
|
|
@@ -128,6 +139,17 @@ def test_the_catalog_carries_every_standing_and_license_type_the_corpus_pins(
|
|
|
128
139
|
)
|
|
129
140
|
|
|
130
141
|
|
|
142
|
+
def test_the_runtime_vocabularies_are_the_pinned_specs() -> None:
|
|
143
|
+
"""The staleness pin on three hand-written Literals.
|
|
144
|
+
|
|
145
|
+
`emit.mjs` reads the corpus's lists out of the pinned spec, so a re-pin that widens a
|
|
146
|
+
vocabulary turns this red instead of leaving a published value unnamed here.
|
|
147
|
+
"""
|
|
148
|
+
assert sorted(DATABASE_FORMATS) == sorted(TESTDATA["formats"])
|
|
149
|
+
assert sorted(STANDINGS) == sorted(TESTDATA["standings"])
|
|
150
|
+
assert sorted(LICENSE_TYPES) == sorted(TESTDATA["license_type"])
|
|
151
|
+
|
|
152
|
+
|
|
131
153
|
# The visibility contract. A private family is one commissioned for a single customer: the
|
|
132
154
|
# server leaves it out of the listing entirely for anyone else, rather than including it
|
|
133
155
|
# with standing 'unlicensed'. All this library has to do is not undo that, which is a real
|
|
@@ -161,7 +183,7 @@ def test_a_listing_is_never_reused_across_clients(make_client: ClientFactory) ->
|
|
|
161
183
|
"""Two keys are two organizations, and they do not see the same catalog.
|
|
162
184
|
|
|
163
185
|
So there is no listing cache at all here, per instance or otherwise: a cached one
|
|
164
|
-
would be wrong the moment a
|
|
186
|
+
would be wrong the moment a license is granted, and a shared one would show an
|
|
165
187
|
organization a family it is not allowed to know exists.
|
|
166
188
|
"""
|
|
167
189
|
assert "a-listing-is-never-reused-across-clients" in TESTDATA["visibility"]["clientRules"]
|
|
@@ -172,7 +194,7 @@ def test_a_listing_is_never_reused_across_clients(make_client: ClientFactory) ->
|
|
|
172
194
|
assert len(first.database.list()) == len(CATALOG)
|
|
173
195
|
assert second.database.list() == [], "the second key answered from the first one's listing"
|
|
174
196
|
assert len(stub.requests) == 2
|
|
175
|
-
# A repeat on the SAME client asks again too: a
|
|
197
|
+
# A repeat on the SAME client asks again too: a license granted between two calls has
|
|
176
198
|
# to show up.
|
|
177
199
|
first.database.list()
|
|
178
200
|
assert len(stub.requests) == 3
|
|
@@ -210,7 +232,7 @@ def test_the_api_key_reaches_the_wire_under_the_bearer_scheme(
|
|
|
210
232
|
def test_a_keyless_client_sends_no_authorization_header(
|
|
211
233
|
make_client: ClientFactory, api_key: str | None
|
|
212
234
|
) -> None:
|
|
213
|
-
"""The key is optional because what this API serves without a
|
|
235
|
+
"""The key is optional because what this API serves without a license is a product
|
|
214
236
|
decision, and a client that could not be built without one would have to change
|
|
215
237
|
shape to follow it. What must never go out is `Bearer ` with nothing after it, which
|
|
216
238
|
reads as a wrong key rather than as none.
|
|
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
|
|
File without changes
|
{internetdata-2.1.0 → internetdata-2.2.0}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_format.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{internetdata-2.1.0 → internetdata-2.2.0}/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
|
{internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/database_version.py
RENAMED
|
File without changes
|
{internetdata-2.1.0 → internetdata-2.2.0}/src/internetdata/_generated/models/db_checksums.py
RENAMED
|
File without changes
|
|
File without changes
|
{internetdata-2.1.0 → internetdata-2.2.0}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|