vpndetection 1.0.0__py3-none-any.whl
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.
- vpndetection/__init__.py +49 -0
- vpndetection/_bogons.py +63 -0
- vpndetection/_core.py +292 -0
- vpndetection/_generated/__init__.py +8 -0
- vpndetection/_generated/api/__init__.py +1 -0
- vpndetection/_generated/api/database/__init__.py +1 -0
- vpndetection/_generated/api/database/database_checksum.py +203 -0
- vpndetection/_generated/api/database/database_metadata.py +205 -0
- vpndetection/_generated/api/database/download_database.py +201 -0
- vpndetection/_generated/api/database/list_databases.py +134 -0
- vpndetection/_generated/api/database/list_downloads.py +166 -0
- vpndetection/_generated/api/lookup/__init__.py +1 -0
- vpndetection/_generated/api/lookup/lookup_ip.py +197 -0
- vpndetection/_generated/client.py +272 -0
- vpndetection/_generated/errors.py +16 -0
- vpndetection/_generated/models/__init__.py +63 -0
- vpndetection/_generated/models/class_detail.py +91 -0
- vpndetection/_generated/models/database_checksum_format.py +9 -0
- vpndetection/_generated/models/database_checksum_response_200.py +87 -0
- vpndetection/_generated/models/database_checksum_response_200_checksums.py +88 -0
- vpndetection/_generated/models/dataset_format_size.py +77 -0
- vpndetection/_generated/models/dataset_format_size_format.py +9 -0
- vpndetection/_generated/models/dataset_metadata.py +167 -0
- vpndetection/_generated/models/dataset_metadata_column.py +80 -0
- vpndetection/_generated/models/dataset_metadata_sample.py +78 -0
- vpndetection/_generated/models/dataset_metadata_sample_additional_property_item.py +45 -0
- vpndetection/_generated/models/dataset_metadata_sample_size.py +47 -0
- vpndetection/_generated/models/dataset_metadata_schema.py +72 -0
- vpndetection/_generated/models/dataset_metadata_size.py +47 -0
- vpndetection/_generated/models/download.py +170 -0
- vpndetection/_generated/models/download_database_format.py +9 -0
- vpndetection/_generated/models/download_outcome.py +13 -0
- vpndetection/_generated/models/error.py +61 -0
- vpndetection/_generated/models/licensed_dataset.py +194 -0
- vpndetection/_generated/models/licensed_dataset_redistribution.py +10 -0
- vpndetection/_generated/models/licensed_dataset_standing.py +10 -0
- vpndetection/_generated/models/licensed_version.py +125 -0
- vpndetection/_generated/models/licensed_version_sample_formats_item.py +9 -0
- vpndetection/_generated/models/list_databases_response_200.py +75 -0
- vpndetection/_generated/models/list_downloads_response_200.py +75 -0
- vpndetection/_generated/models/lookup_error.py +62 -0
- vpndetection/_generated/models/lookup_response.py +295 -0
- vpndetection/_generated/models/proxy_detail.py +129 -0
- vpndetection/_generated/models/vpn_detail.py +101 -0
- vpndetection/_generated/types.py +54 -0
- vpndetection/aio.py +335 -0
- vpndetection/bogon.py +85 -0
- vpndetection/client.py +332 -0
- vpndetection/errors.py +119 -0
- vpndetection/models.py +190 -0
- vpndetection/py.typed +0 -0
- vpndetection-1.0.0.dist-info/METADATA +225 -0
- vpndetection-1.0.0.dist-info/RECORD +55 -0
- vpndetection-1.0.0.dist-info/WHEEL +4 -0
- vpndetection-1.0.0.dist-info/licenses/LICENSE +21 -0
vpndetection/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""The official Python client library for the VPNDetection API.
|
|
2
|
+
|
|
3
|
+
from vpndetection import VPNDetection
|
|
4
|
+
|
|
5
|
+
with VPNDetection() as client:
|
|
6
|
+
print(client.lookup("45.83.91.1").is_vpn)
|
|
7
|
+
|
|
8
|
+
No API key is needed to start. See https://vpndetection.io for the API, and the README
|
|
9
|
+
for batching, caching and the licensed dataset downloads.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ._core import DEFAULT_BASE_URL
|
|
13
|
+
from ._generated.models.dataset_format_size import DatasetFormatSize
|
|
14
|
+
from ._generated.models.dataset_metadata import DatasetMetadata
|
|
15
|
+
from ._generated.models.dataset_metadata_column import DatasetMetadataColumn
|
|
16
|
+
from ._generated.models.download import Download
|
|
17
|
+
from ._generated.models.licensed_dataset import LicensedDataset
|
|
18
|
+
from ._generated.models.licensed_version import LicensedVersion
|
|
19
|
+
from .aio import AsyncDatabaseApi, AsyncVPNDetection
|
|
20
|
+
from .bogon import is_bogon
|
|
21
|
+
from .client import DatabaseApi, VPNDetection
|
|
22
|
+
from .errors import ErrorKind, VPNDetectionError
|
|
23
|
+
from .models import ClassDetail, Flag, Format, ProxyDetail, Result, VpnDetail
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"DEFAULT_BASE_URL",
|
|
29
|
+
"AsyncDatabaseApi",
|
|
30
|
+
"AsyncVPNDetection",
|
|
31
|
+
"ClassDetail",
|
|
32
|
+
"DatabaseApi",
|
|
33
|
+
"DatasetFormatSize",
|
|
34
|
+
"DatasetMetadata",
|
|
35
|
+
"DatasetMetadataColumn",
|
|
36
|
+
"Download",
|
|
37
|
+
"ErrorKind",
|
|
38
|
+
"Flag",
|
|
39
|
+
"Format",
|
|
40
|
+
"LicensedDataset",
|
|
41
|
+
"LicensedVersion",
|
|
42
|
+
"ProxyDetail",
|
|
43
|
+
"Result",
|
|
44
|
+
"VPNDetection",
|
|
45
|
+
"VPNDetectionError",
|
|
46
|
+
"VpnDetail",
|
|
47
|
+
"__version__",
|
|
48
|
+
"is_bogon",
|
|
49
|
+
]
|
vpndetection/_bogons.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Generated by sdk/common. Do not edit.
|
|
2
|
+
# The canonical list lives in the monorepo and is checked against both
|
|
3
|
+
# upstream definitions on every regeneration.
|
|
4
|
+
|
|
5
|
+
BOGON_V4: tuple[str, ...] = (
|
|
6
|
+
"0.0.0.0/8",
|
|
7
|
+
"10.0.0.0/8",
|
|
8
|
+
"100.64.0.0/10",
|
|
9
|
+
"127.0.0.0/8",
|
|
10
|
+
"169.254.0.0/16",
|
|
11
|
+
"172.16.0.0/12",
|
|
12
|
+
"192.0.0.0/24",
|
|
13
|
+
"192.0.2.0/24",
|
|
14
|
+
"192.168.0.0/16",
|
|
15
|
+
"198.18.0.0/15",
|
|
16
|
+
"198.51.100.0/24",
|
|
17
|
+
"203.0.113.0/24",
|
|
18
|
+
"224.0.0.0/4",
|
|
19
|
+
"240.0.0.0/4",
|
|
20
|
+
"255.255.255.255/32",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
BOGON_V6: tuple[str, ...] = (
|
|
24
|
+
"::/128",
|
|
25
|
+
"::1/128",
|
|
26
|
+
"::ffff:0:0/96",
|
|
27
|
+
"::/96",
|
|
28
|
+
"100::/64",
|
|
29
|
+
"2001:10::/28",
|
|
30
|
+
"2001:db8::/32",
|
|
31
|
+
"fc00::/7",
|
|
32
|
+
"fe80::/10",
|
|
33
|
+
"fec0::/10",
|
|
34
|
+
"ff00::/8",
|
|
35
|
+
"2002::/24",
|
|
36
|
+
"2002:a00::/24",
|
|
37
|
+
"2002:7f00::/24",
|
|
38
|
+
"2002:a9fe::/32",
|
|
39
|
+
"2002:ac10::/28",
|
|
40
|
+
"2002:c000::/40",
|
|
41
|
+
"2002:c000:200::/40",
|
|
42
|
+
"2002:c0a8::/32",
|
|
43
|
+
"2002:c612::/31",
|
|
44
|
+
"2002:c633:6400::/40",
|
|
45
|
+
"2002:cb00:7100::/40",
|
|
46
|
+
"2002:e000::/20",
|
|
47
|
+
"2002:f000::/20",
|
|
48
|
+
"2002:ffff:ffff::/48",
|
|
49
|
+
"2001::/40",
|
|
50
|
+
"2001:0:a00::/40",
|
|
51
|
+
"2001:0:7f00::/40",
|
|
52
|
+
"2001:0:a9fe::/48",
|
|
53
|
+
"2001:0:ac10::/44",
|
|
54
|
+
"2001:0:c000::/56",
|
|
55
|
+
"2001:0:c000:200::/56",
|
|
56
|
+
"2001:0:c0a8::/48",
|
|
57
|
+
"2001:0:c612::/47",
|
|
58
|
+
"2001:0:c633:6400::/56",
|
|
59
|
+
"2001:0:cb00:7100::/56",
|
|
60
|
+
"2001:0:e000::/36",
|
|
61
|
+
"2001:0:f000::/36",
|
|
62
|
+
"2001:0:ffff:ffff::/64",
|
|
63
|
+
)
|
vpndetection/_core.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Plumbing the sync and the async client both need: transport wiring, response
|
|
2
|
+
unwrapping, the retry policy, and the per-instance cache."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import contextlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import threading
|
|
10
|
+
from collections.abc import Awaitable, Callable, Iterator
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import IO, Any, TypeVar, cast
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
from cachetools import TTLCache
|
|
16
|
+
|
|
17
|
+
from ._generated.client import AuthenticatedClient, Client
|
|
18
|
+
from ._generated.models.download import Download
|
|
19
|
+
from ._generated.models.licensed_dataset import LicensedDataset
|
|
20
|
+
from ._generated.types import Response
|
|
21
|
+
from .errors import VPNDetectionError, error_from_response
|
|
22
|
+
from .models import Result
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL = "https://api.vpndetection.io"
|
|
25
|
+
DEFAULT_CONCURRENCY = 8
|
|
26
|
+
DEFAULT_RETRIES = 2
|
|
27
|
+
DEFAULT_CACHE_MAX_SIZE = 10_000
|
|
28
|
+
DEFAULT_CACHE_TTL = 3600.0
|
|
29
|
+
DEFAULT_TIMEOUT = 10.0
|
|
30
|
+
DEFAULT_DOWNLOADS_LIMIT = 50
|
|
31
|
+
|
|
32
|
+
# One chunk of a transfer, and therefore the ceiling on what a download of any size
|
|
33
|
+
# costs in memory.
|
|
34
|
+
TRANSFER_CHUNK_BYTES = 1 << 20
|
|
35
|
+
|
|
36
|
+
_BACKOFF_BASE = 1.0
|
|
37
|
+
|
|
38
|
+
T = TypeVar("T")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build_client(
|
|
42
|
+
api_key: str | None,
|
|
43
|
+
base_url: str,
|
|
44
|
+
timeout: float | None,
|
|
45
|
+
transport: httpx.BaseTransport | httpx.AsyncBaseTransport | None,
|
|
46
|
+
) -> AuthenticatedClient:
|
|
47
|
+
"""The generated client, wired for one of our clients.
|
|
48
|
+
|
|
49
|
+
Every generated endpoint function types `client` as `AuthenticatedClient` because
|
|
50
|
+
every operation lists a security scheme, but the lookup endpoint also accepts `{}`
|
|
51
|
+
and a keyless caller must not send an empty `Authorization` header. The two classes
|
|
52
|
+
are interchangeable where the endpoints use them, so the keyless one is built as
|
|
53
|
+
`Client` and the cast lives here instead of at every call site.
|
|
54
|
+
"""
|
|
55
|
+
httpx_args: dict[str, Any] = {}
|
|
56
|
+
if transport is not None:
|
|
57
|
+
httpx_args["transport"] = transport
|
|
58
|
+
if api_key is None:
|
|
59
|
+
return cast(
|
|
60
|
+
AuthenticatedClient,
|
|
61
|
+
Client(base_url=base_url, timeout=httpx.Timeout(timeout), httpx_args=httpx_args),
|
|
62
|
+
)
|
|
63
|
+
return AuthenticatedClient(
|
|
64
|
+
base_url=base_url,
|
|
65
|
+
token=api_key,
|
|
66
|
+
timeout=httpx.Timeout(timeout),
|
|
67
|
+
httpx_args=httpx_args,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_transfer_client(
|
|
72
|
+
timeout: float | None, transport: httpx.BaseTransport | None
|
|
73
|
+
) -> httpx.Client:
|
|
74
|
+
"""A SECOND client, holding no credential, for the object-storage leg of a download.
|
|
75
|
+
|
|
76
|
+
The API answers a download with a `302` to a presigned URL, and that URL authorizes
|
|
77
|
+
itself. Following the redirect on the API client would forward the key to a host
|
|
78
|
+
with no business holding it, so the second request is made from here instead, where
|
|
79
|
+
there is no `Authorization` header to send.
|
|
80
|
+
|
|
81
|
+
Only the connect phase keeps the client's timeout. That timeout is a sane bound on a
|
|
82
|
+
lookup and the wrong one on a body that routinely runs to gigabytes, which would
|
|
83
|
+
otherwise be cut off mid-transfer. Redirects ARE followed here, unlike on the API
|
|
84
|
+
client: object storage behind a CDN answers one, and there is no credential to leak
|
|
85
|
+
by going along with it.
|
|
86
|
+
"""
|
|
87
|
+
return httpx.Client(
|
|
88
|
+
timeout=httpx.Timeout(None, connect=timeout),
|
|
89
|
+
transport=transport,
|
|
90
|
+
follow_redirects=True,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_async_transfer_client(
|
|
95
|
+
timeout: float | None, transport: httpx.AsyncBaseTransport | None
|
|
96
|
+
) -> httpx.AsyncClient:
|
|
97
|
+
"""`build_transfer_client`, for asyncio."""
|
|
98
|
+
return httpx.AsyncClient(
|
|
99
|
+
timeout=httpx.Timeout(None, connect=timeout),
|
|
100
|
+
transport=transport,
|
|
101
|
+
follow_redirects=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def storage_refusal(res: httpx.Response) -> VPNDetectionError:
|
|
106
|
+
"""What object storage refusing a download link becomes.
|
|
107
|
+
|
|
108
|
+
The body is deliberately left unread: the status is what separates a lapsed link
|
|
109
|
+
from a refused one, and nothing bounds the size of an error page.
|
|
110
|
+
"""
|
|
111
|
+
return error_from_response(
|
|
112
|
+
res.status_code,
|
|
113
|
+
res.headers,
|
|
114
|
+
{"error": f"object storage refused the download link with status {res.status_code}"},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def assert_whole_transfer(res: httpx.Response, written: int) -> None:
|
|
119
|
+
"""Check what arrived against what was promised.
|
|
120
|
+
|
|
121
|
+
A transfer that dies mid-body can reach a client as a plain end of stream, and a
|
|
122
|
+
short file that looks complete is worse than no file at all: the next run reads it
|
|
123
|
+
as a whole dataset.
|
|
124
|
+
|
|
125
|
+
Skipped when the body was decoded on the way in, because `Content-Length` then
|
|
126
|
+
describes the ENCODED bytes and disagreeing with it is correct rather than short.
|
|
127
|
+
A chunked response declares no length; httpx raises for itself when one of those is
|
|
128
|
+
cut off.
|
|
129
|
+
"""
|
|
130
|
+
declared = res.headers.get("content-length")
|
|
131
|
+
encoding = res.headers.get("content-encoding", "identity").strip().lower()
|
|
132
|
+
if declared is None or encoding not in ("", "identity"):
|
|
133
|
+
return
|
|
134
|
+
try:
|
|
135
|
+
expected = int(declared)
|
|
136
|
+
except ValueError:
|
|
137
|
+
return
|
|
138
|
+
if expected != written:
|
|
139
|
+
raise VPNDetectionError(
|
|
140
|
+
"network",
|
|
141
|
+
f"the transfer ended after {written} of {expected} bytes",
|
|
142
|
+
res.status_code,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@contextlib.contextmanager
|
|
147
|
+
def part_file(destination: str | os.PathLike[str]) -> Iterator[IO[bytes]]:
|
|
148
|
+
"""A download's bytes, landing beside `destination` and moved onto it at the end.
|
|
149
|
+
|
|
150
|
+
Two failures this prevents, and only the first is the obvious one. A transfer that
|
|
151
|
+
dies half way leaves no truncated file carrying the real name. And a refresh that
|
|
152
|
+
fails leaves yesterday's good copy untouched, which opening the destination itself
|
|
153
|
+
could not do: that truncates it before the first byte of the new one arrives.
|
|
154
|
+
"""
|
|
155
|
+
partial = os.fspath(destination) + ".part"
|
|
156
|
+
try:
|
|
157
|
+
with open(partial, "wb") as sink:
|
|
158
|
+
yield sink
|
|
159
|
+
os.replace(partial, destination)
|
|
160
|
+
except BaseException:
|
|
161
|
+
Path(partial).unlink(missing_ok=True)
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def send(call: Callable[[], Response[Any]]) -> Response[Any]:
|
|
166
|
+
"""One generated endpoint call.
|
|
167
|
+
|
|
168
|
+
The generated code eagerly parses the body of every DOCUMENTED status as JSON, so a
|
|
169
|
+
429 or a 400 carrying an intermediary's HTML error page raises straight out of it.
|
|
170
|
+
That is a failed request rather than a bug in the caller's code, so it becomes the
|
|
171
|
+
one error type here.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
return call()
|
|
175
|
+
except ValueError as exc:
|
|
176
|
+
raise VPNDetectionError(
|
|
177
|
+
"server_error", f"the API answered with a body this client could not read: {exc}"
|
|
178
|
+
) from exc
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def send_async(call: Callable[[], Awaitable[Response[Any]]]) -> Response[Any]:
|
|
182
|
+
"""`send`, awaited."""
|
|
183
|
+
try:
|
|
184
|
+
return await call()
|
|
185
|
+
except ValueError as exc:
|
|
186
|
+
raise VPNDetectionError(
|
|
187
|
+
"server_error", f"the API answered with a body this client could not read: {exc}"
|
|
188
|
+
) from exc
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def unwrap(res: Response[Any]) -> dict[str, Any]:
|
|
192
|
+
"""The response body as it came off the wire, or the failure it describes."""
|
|
193
|
+
body = _decode(res.content)
|
|
194
|
+
status = int(res.status_code)
|
|
195
|
+
if not 200 <= status < 300:
|
|
196
|
+
raise error_from_response(status, _headers(res), body)
|
|
197
|
+
if not isinstance(body, dict):
|
|
198
|
+
raise VPNDetectionError("server_error", "the API answered with a non-object body", status)
|
|
199
|
+
return body
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def as_error(exc: VPNDetectionError | httpx.HTTPError) -> VPNDetectionError:
|
|
203
|
+
"""A transport failure, as the one error type this library raises.
|
|
204
|
+
|
|
205
|
+
Deliberately narrow: anything else is a bug rather than a failed request, and
|
|
206
|
+
turning it into a `network` error here would hide it behind a retry.
|
|
207
|
+
"""
|
|
208
|
+
if isinstance(exc, VPNDetectionError):
|
|
209
|
+
return exc
|
|
210
|
+
return VPNDetectionError("network", str(exc) or type(exc).__name__)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def parse_body(body: dict[str, Any], parse: Callable[[dict[str, Any]], T]) -> T:
|
|
214
|
+
"""A served body through its generated model, with a malformed one reported as the
|
|
215
|
+
server's failure rather than as a traceback out of the generated code."""
|
|
216
|
+
try:
|
|
217
|
+
return parse(body)
|
|
218
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
219
|
+
raise VPNDetectionError("server_error", f"malformed response from the API: {exc}") from exc
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def redirect_location(res: Response[Any]) -> str:
|
|
223
|
+
"""Where a `302` points, or whatever the API said instead."""
|
|
224
|
+
location: str | None = _headers(res).get("location")
|
|
225
|
+
if int(res.status_code) == 302 and location:
|
|
226
|
+
return location
|
|
227
|
+
unwrap(res)
|
|
228
|
+
raise VPNDetectionError(
|
|
229
|
+
"server_error", "expected a redirect to object storage", int(res.status_code)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def datasets_of(body: dict[str, Any]) -> list[LicensedDataset]:
|
|
234
|
+
return [LicensedDataset.from_dict(d) for d in body["datasets"]]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def downloads_of(body: dict[str, Any]) -> list[Download]:
|
|
238
|
+
return [Download.from_dict(d) for d in body["downloads"]]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def checksums_of(body: dict[str, Any]) -> dict[str, str]:
|
|
242
|
+
return dict(body["checksums"])
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def retry_delay(err: VPNDetectionError, attempt: int, retries: int) -> float | None:
|
|
246
|
+
"""How long to wait before attempt `attempt + 1`, or None when there must not be one.
|
|
247
|
+
|
|
248
|
+
A server-supplied `Retry-After` wins over the backoff schedule outright: it is the
|
|
249
|
+
only thing that makes a 429 worth retrying at all, so second-guessing it with a
|
|
250
|
+
shorter wait would just spend the next attempt on the same rejection.
|
|
251
|
+
"""
|
|
252
|
+
if attempt >= retries or not err.retryable:
|
|
253
|
+
return None
|
|
254
|
+
if err.retry_after_seconds is not None:
|
|
255
|
+
return err.retry_after_seconds
|
|
256
|
+
return _BACKOFF_BASE * (2.0**attempt)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class Cache:
|
|
260
|
+
"""A per-client result cache.
|
|
261
|
+
|
|
262
|
+
Never global or static: two clients with different keys are on different plans and
|
|
263
|
+
entitled to different fields, so a shared cache would serve one of them the other's
|
|
264
|
+
shape. The lock is not optional, because the sync batch drives lookups from a thread
|
|
265
|
+
pool and `TTLCache` is not thread-safe.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(self, max_size: int, ttl: float) -> None:
|
|
269
|
+
self._entries: TTLCache[str, Result] = TTLCache(maxsize=max_size, ttl=ttl)
|
|
270
|
+
self._lock = threading.Lock()
|
|
271
|
+
|
|
272
|
+
def get(self, ip: str) -> Result | None:
|
|
273
|
+
with self._lock:
|
|
274
|
+
return self._entries.get(ip)
|
|
275
|
+
|
|
276
|
+
def put(self, ip: str, result: Result) -> None:
|
|
277
|
+
with self._lock:
|
|
278
|
+
self._entries[ip] = result
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# The generated Response declares a plain MutableMapping, but always carries httpx's
|
|
282
|
+
# case-insensitive Headers. Rebuilding one keeps a header lookup case-blind whichever it
|
|
283
|
+
# turns out to be, which matters for `Retry-After`.
|
|
284
|
+
def _headers(res: Response[Any]) -> httpx.Headers:
|
|
285
|
+
return httpx.Headers(res.headers)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _decode(content: bytes) -> Any:
|
|
289
|
+
try:
|
|
290
|
+
return json.loads(content)
|
|
291
|
+
except ValueError:
|
|
292
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains methods for accessing the API"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains endpoint functions for accessing the API"""
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from ... import errors
|
|
7
|
+
from ...client import AuthenticatedClient, Client
|
|
8
|
+
from ...models.database_checksum_format import DatabaseChecksumFormat
|
|
9
|
+
from ...models.database_checksum_response_200 import DatabaseChecksumResponse200
|
|
10
|
+
from ...models.error import Error
|
|
11
|
+
from ...types import UNSET, Response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_kwargs(
|
|
15
|
+
*,
|
|
16
|
+
id: str,
|
|
17
|
+
format_: DatabaseChecksumFormat,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
|
|
20
|
+
params: dict[str, Any] = {}
|
|
21
|
+
|
|
22
|
+
params["id"] = id
|
|
23
|
+
|
|
24
|
+
json_format_ = format_.value
|
|
25
|
+
params["format"] = json_format_
|
|
26
|
+
|
|
27
|
+
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
|
|
28
|
+
|
|
29
|
+
_kwargs: dict[str, Any] = {
|
|
30
|
+
"method": "get",
|
|
31
|
+
"url": "/api/v1/database/checksum",
|
|
32
|
+
"params": params,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return _kwargs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _parse_response(
|
|
39
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
40
|
+
) -> DatabaseChecksumResponse200 | Error | None:
|
|
41
|
+
if response.status_code == 200:
|
|
42
|
+
response_200 = DatabaseChecksumResponse200.from_dict(response.json())
|
|
43
|
+
|
|
44
|
+
return response_200
|
|
45
|
+
|
|
46
|
+
if response.status_code == 400:
|
|
47
|
+
response_400 = Error.from_dict(response.json())
|
|
48
|
+
|
|
49
|
+
return response_400
|
|
50
|
+
|
|
51
|
+
if response.status_code == 401:
|
|
52
|
+
response_401 = Error.from_dict(response.json())
|
|
53
|
+
|
|
54
|
+
return response_401
|
|
55
|
+
|
|
56
|
+
if response.status_code == 403:
|
|
57
|
+
response_403 = Error.from_dict(response.json())
|
|
58
|
+
|
|
59
|
+
return response_403
|
|
60
|
+
|
|
61
|
+
if response.status_code == 404:
|
|
62
|
+
response_404 = Error.from_dict(response.json())
|
|
63
|
+
|
|
64
|
+
return response_404
|
|
65
|
+
|
|
66
|
+
if response.status_code == 503:
|
|
67
|
+
response_503 = Error.from_dict(response.json())
|
|
68
|
+
|
|
69
|
+
return response_503
|
|
70
|
+
|
|
71
|
+
if client.raise_on_unexpected_status:
|
|
72
|
+
raise errors.UnexpectedStatus(response.status_code, response.content)
|
|
73
|
+
else:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_response(
|
|
78
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
79
|
+
) -> Response[DatabaseChecksumResponse200 | Error]:
|
|
80
|
+
return Response(
|
|
81
|
+
status_code=HTTPStatus(response.status_code),
|
|
82
|
+
content=response.content,
|
|
83
|
+
headers=response.headers,
|
|
84
|
+
parsed=_parse_response(client=client, response=response),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sync_detailed(
|
|
89
|
+
*,
|
|
90
|
+
client: AuthenticatedClient,
|
|
91
|
+
id: str,
|
|
92
|
+
format_: DatabaseChecksumFormat,
|
|
93
|
+
) -> Response[DatabaseChecksumResponse200 | Error]:
|
|
94
|
+
"""Checksums for one published file, to verify a download
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
id (str):
|
|
98
|
+
format_ (DatabaseChecksumFormat):
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
102
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Response[DatabaseChecksumResponse200 | Error]
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
kwargs = _get_kwargs(
|
|
109
|
+
id=id,
|
|
110
|
+
format_=format_,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
response = client.get_httpx_client().request(
|
|
114
|
+
**kwargs,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return _build_response(client=client, response=response)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def sync(
|
|
121
|
+
*,
|
|
122
|
+
client: AuthenticatedClient,
|
|
123
|
+
id: str,
|
|
124
|
+
format_: DatabaseChecksumFormat,
|
|
125
|
+
) -> DatabaseChecksumResponse200 | Error | None:
|
|
126
|
+
"""Checksums for one published file, to verify a download
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
id (str):
|
|
130
|
+
format_ (DatabaseChecksumFormat):
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
134
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
DatabaseChecksumResponse200 | Error
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
return sync_detailed(
|
|
141
|
+
client=client,
|
|
142
|
+
id=id,
|
|
143
|
+
format_=format_,
|
|
144
|
+
).parsed
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def asyncio_detailed(
|
|
148
|
+
*,
|
|
149
|
+
client: AuthenticatedClient,
|
|
150
|
+
id: str,
|
|
151
|
+
format_: DatabaseChecksumFormat,
|
|
152
|
+
) -> Response[DatabaseChecksumResponse200 | Error]:
|
|
153
|
+
"""Checksums for one published file, to verify a download
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
id (str):
|
|
157
|
+
format_ (DatabaseChecksumFormat):
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
161
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Response[DatabaseChecksumResponse200 | Error]
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
kwargs = _get_kwargs(
|
|
168
|
+
id=id,
|
|
169
|
+
format_=format_,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
response = await client.get_async_httpx_client().request(**kwargs)
|
|
173
|
+
|
|
174
|
+
return _build_response(client=client, response=response)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def asyncio(
|
|
178
|
+
*,
|
|
179
|
+
client: AuthenticatedClient,
|
|
180
|
+
id: str,
|
|
181
|
+
format_: DatabaseChecksumFormat,
|
|
182
|
+
) -> DatabaseChecksumResponse200 | Error | None:
|
|
183
|
+
"""Checksums for one published file, to verify a download
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
id (str):
|
|
187
|
+
format_ (DatabaseChecksumFormat):
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
191
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
DatabaseChecksumResponse200 | Error
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
await asyncio_detailed(
|
|
199
|
+
client=client,
|
|
200
|
+
id=id,
|
|
201
|
+
format_=format_,
|
|
202
|
+
)
|
|
203
|
+
).parsed
|