internetdata 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.
Files changed (46) hide show
  1. internetdata/__init__.py +49 -0
  2. internetdata/_core.py +287 -0
  3. internetdata/_generated/__init__.py +8 -0
  4. internetdata/_generated/api/__init__.py +1 -0
  5. internetdata/_generated/api/database_v_2/__init__.py +1 -0
  6. internetdata/_generated/api/database_v_2/database_checksum_v2.py +203 -0
  7. internetdata/_generated/api/database_v_2/database_metadata_v2.py +205 -0
  8. internetdata/_generated/api/database_v_2/download_database_v2.py +213 -0
  9. internetdata/_generated/api/database_v_2/list_databases.py +162 -0
  10. internetdata/_generated/api/database_v_2/list_downloads.py +178 -0
  11. internetdata/_generated/client.py +272 -0
  12. internetdata/_generated/errors.py +16 -0
  13. internetdata/_generated/models/__init__.py +53 -0
  14. internetdata/_generated/models/database.py +233 -0
  15. internetdata/_generated/models/database_checksum_v2_format.py +9 -0
  16. internetdata/_generated/models/database_checksum_v2_response_200.py +85 -0
  17. internetdata/_generated/models/database_checksum_v2_response_200_format.py +9 -0
  18. internetdata/_generated/models/database_metadata.py +132 -0
  19. internetdata/_generated/models/database_metadata_column.py +80 -0
  20. internetdata/_generated/models/database_metadata_sample.py +78 -0
  21. internetdata/_generated/models/database_metadata_sample_additional_property_item.py +45 -0
  22. internetdata/_generated/models/database_metadata_schema.py +72 -0
  23. internetdata/_generated/models/database_metadata_size.py +47 -0
  24. internetdata/_generated/models/database_redistribution_type_1.py +10 -0
  25. internetdata/_generated/models/database_redistribution_type_2_type_1.py +10 -0
  26. internetdata/_generated/models/database_redistribution_type_3_type_1.py +10 -0
  27. internetdata/_generated/models/database_standing.py +10 -0
  28. internetdata/_generated/models/database_version.py +97 -0
  29. internetdata/_generated/models/database_version_formats_item.py +9 -0
  30. internetdata/_generated/models/db_checksums.py +85 -0
  31. internetdata/_generated/models/download.py +161 -0
  32. internetdata/_generated/models/download_database_v2_format.py +9 -0
  33. internetdata/_generated/models/download_outcome.py +13 -0
  34. internetdata/_generated/models/error.py +62 -0
  35. internetdata/_generated/models/list_databases_response_200.py +75 -0
  36. internetdata/_generated/models/list_downloads_response_200.py +75 -0
  37. internetdata/_generated/types.py +54 -0
  38. internetdata/aio.py +268 -0
  39. internetdata/client.py +278 -0
  40. internetdata/errors.py +128 -0
  41. internetdata/models.py +197 -0
  42. internetdata/py.typed +0 -0
  43. internetdata-1.0.0.dist-info/METADATA +184 -0
  44. internetdata-1.0.0.dist-info/RECORD +46 -0
  45. internetdata-1.0.0.dist-info/WHEEL +4 -0
  46. internetdata-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,49 @@
1
+ """The official Python client library for the InternetData API.
2
+
3
+ from internetdata import InternetData
4
+
5
+ with InternetData(api_key) as client:
6
+ for db in client.database.list():
7
+ print(db.base, db.standing)
8
+
9
+ See https://internetdata.io for the API, and the README for downloads, checksums and
10
+ what the catalog does and does not show you.
11
+ """
12
+
13
+ from ._core import DEFAULT_BASE_URL
14
+ from .aio import AsyncDatabaseApi, AsyncInternetData
15
+ from .client import DatabaseApi, InternetData
16
+ from .errors import ErrorKind, InternetDataError
17
+ from .models import (
18
+ Database,
19
+ DatabaseMetadata,
20
+ DatabaseVersion,
21
+ Download,
22
+ Format,
23
+ MetadataColumn,
24
+ Outcome,
25
+ Redistribution,
26
+ Standing,
27
+ )
28
+
29
+ __version__ = "1.0.0"
30
+
31
+ __all__ = [
32
+ "DEFAULT_BASE_URL",
33
+ "AsyncDatabaseApi",
34
+ "AsyncInternetData",
35
+ "Database",
36
+ "DatabaseApi",
37
+ "DatabaseMetadata",
38
+ "DatabaseVersion",
39
+ "Download",
40
+ "ErrorKind",
41
+ "Format",
42
+ "InternetData",
43
+ "InternetDataError",
44
+ "MetadataColumn",
45
+ "Outcome",
46
+ "Redistribution",
47
+ "Standing",
48
+ "__version__",
49
+ ]
internetdata/_core.py ADDED
@@ -0,0 +1,287 @@
1
+ """Plumbing the sync and the async client both need: transport wiring, response
2
+ unwrapping, and the retry policy."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import contextlib
7
+ import json
8
+ import os
9
+ from collections.abc import Awaitable, Callable, Iterator
10
+ from pathlib import Path
11
+ from typing import IO, Any, TypeVar, cast
12
+
13
+ import httpx
14
+
15
+ from ._generated.client import AuthenticatedClient, Client
16
+ from ._generated.types import Response
17
+ from .errors import InternetDataError, error_from_response
18
+ from .models import Database, Download, to_database, to_download
19
+
20
+ DEFAULT_BASE_URL = "https://internetdata.io"
21
+ DEFAULT_RETRIES = 2
22
+ DEFAULT_TIMEOUT = 10.0
23
+ DEFAULT_DOWNLOADS_LIMIT = 50
24
+
25
+ # One chunk of a transfer, and therefore the ceiling on what a download of any size
26
+ # costs in memory.
27
+ TRANSFER_CHUNK_BYTES = 1 << 20
28
+
29
+ _BACKOFF_BASE = 1.0
30
+
31
+ T = TypeVar("T")
32
+
33
+
34
+ def build_client(
35
+ api_key: str | None,
36
+ base_url: str,
37
+ timeout: float | None,
38
+ transport: httpx.BaseTransport | httpx.AsyncBaseTransport | None,
39
+ ) -> AuthenticatedClient:
40
+ """The generated client, wired for one of ours.
41
+
42
+ Every generated endpoint function types `client` as `AuthenticatedClient` because
43
+ every operation lists a security scheme, but a keyless caller must send NO
44
+ `Authorization` header rather than an empty one: `Bearer ` with nothing after it is
45
+ a 401 that reads as a wrong key. The two classes are interchangeable where the
46
+ endpoints use them, so the keyless one is built as `Client` and the cast lives here
47
+ instead of at every call site.
48
+
49
+ The transport is injected through `httpx_args` rather than with
50
+ `set_httpx_client()`, which silently bypasses auth: the generated client only adds
51
+ the `Authorization` header when it CONSTRUCTS the httpx client itself.
52
+ """
53
+ httpx_args: dict[str, Any] = {}
54
+ if transport is not None:
55
+ httpx_args["transport"] = transport
56
+ if not api_key:
57
+ return cast(
58
+ AuthenticatedClient,
59
+ Client(base_url=base_url, timeout=httpx.Timeout(timeout), httpx_args=httpx_args),
60
+ )
61
+ return AuthenticatedClient(
62
+ base_url=base_url,
63
+ token=api_key,
64
+ timeout=httpx.Timeout(timeout),
65
+ httpx_args=httpx_args,
66
+ )
67
+
68
+
69
+ def build_transfer_client(
70
+ timeout: float | None, transport: httpx.BaseTransport | None
71
+ ) -> httpx.Client:
72
+ """A SECOND client, holding no credential, for the object-storage leg of a download.
73
+
74
+ The API answers a download with a `302` to a presigned URL, and that URL authorizes
75
+ itself. Following the redirect on the API client would forward the key to a host with
76
+ no business holding it. Worth more than tidiness: object storage answers 400 to a
77
+ request carrying both a presigned signature and an `Authorization` header, so
78
+ forwarding the key does not merely leak it, it breaks the download.
79
+
80
+ Only the connect phase keeps the client's timeout. That timeout is a sane bound on a
81
+ metadata call and the wrong one on a body that reaches gigabytes, which would
82
+ otherwise be cut off mid-transfer. Redirects ARE followed here, unlike on the API
83
+ client: object storage behind a CDN answers one, and there is no credential to leak
84
+ by going along with it.
85
+ """
86
+ return httpx.Client(
87
+ timeout=httpx.Timeout(None, connect=timeout),
88
+ transport=transport,
89
+ follow_redirects=True,
90
+ )
91
+
92
+
93
+ def build_async_transfer_client(
94
+ timeout: float | None, transport: httpx.AsyncBaseTransport | None
95
+ ) -> httpx.AsyncClient:
96
+ """`build_transfer_client`, for asyncio."""
97
+ return httpx.AsyncClient(
98
+ timeout=httpx.Timeout(None, connect=timeout),
99
+ transport=transport,
100
+ follow_redirects=True,
101
+ )
102
+
103
+
104
+ def storage_refusal(res: httpx.Response) -> InternetDataError:
105
+ """What object storage refusing a download link becomes.
106
+
107
+ The body is deliberately left unread: the status is what separates a lapsed link from
108
+ a refused one, and nothing bounds the size of an error page.
109
+ """
110
+ return error_from_response(
111
+ res.status_code,
112
+ res.headers,
113
+ {"rc": f"object storage refused the download link with status {res.status_code}"},
114
+ )
115
+
116
+
117
+ def assert_whole_transfer(res: httpx.Response, written: int) -> None:
118
+ """Check what arrived against what was promised.
119
+
120
+ A transfer that dies mid-body can reach a client as a plain end of stream, and a
121
+ short file that looks complete is worse than no file at all: the next run reads it as
122
+ a whole database.
123
+
124
+ Skipped when the body was decoded on the way in, because `Content-Length` then
125
+ describes the ENCODED bytes and disagreeing with it is correct rather than short. A
126
+ chunked response declares no length; httpx raises for itself when one of those is cut
127
+ off.
128
+ """
129
+ declared = res.headers.get("content-length")
130
+ encoding = res.headers.get("content-encoding", "identity").strip().lower()
131
+ if declared is None or encoding not in ("", "identity"):
132
+ return
133
+ try:
134
+ expected = int(declared)
135
+ except ValueError:
136
+ return
137
+ if expected != written:
138
+ raise InternetDataError(
139
+ "network",
140
+ f"the transfer ended after {written} of {expected} bytes",
141
+ res.status_code,
142
+ )
143
+
144
+
145
+ @contextlib.contextmanager
146
+ def part_file(destination: str | os.PathLike[str]) -> Iterator[IO[bytes]]:
147
+ """A download's bytes, landing beside `destination` and moved onto it at the end.
148
+
149
+ Two failures this prevents, and only the first is the obvious one. A transfer that
150
+ dies half way leaves no truncated file carrying the real name. And a refresh that
151
+ fails leaves yesterday's good copy untouched, which opening the destination itself
152
+ could not do: that truncates it before the first byte of the new one arrives.
153
+ """
154
+ partial = os.fspath(destination) + ".part"
155
+ try:
156
+ with open(partial, "wb") as sink:
157
+ yield sink
158
+ os.replace(partial, destination)
159
+ except BaseException:
160
+ Path(partial).unlink(missing_ok=True)
161
+ raise
162
+
163
+
164
+ def send(call: Callable[[], Response[Any]]) -> Response[Any]:
165
+ """One generated endpoint call.
166
+
167
+ The generated code eagerly decodes the body of every DOCUMENTED status into its model
168
+ before anything here sees the response, and it raises three different ways doing it: a
169
+ 503 carrying an intermediary's HTML error page is a `ValueError` out of `json()`, a
170
+ 200 missing a required key is a `KeyError`, and a `standing` this pinned spec predates
171
+ is a `ValueError` out of an enum constructor. All three are the server sending
172
+ something this client cannot read, which is a failed request rather than a bug in the
173
+ caller's code, so all three become the one error type here.
174
+
175
+ The lambda does nothing but call the generated function, which is what keeps this
176
+ catch from swallowing a fault of our own.
177
+ """
178
+ try:
179
+ return call()
180
+ except (KeyError, TypeError, ValueError) as exc:
181
+ raise malformed(exc) from exc
182
+
183
+
184
+ async def send_async(call: Callable[[], Awaitable[Response[Any]]]) -> Response[Any]:
185
+ """`send`, awaited."""
186
+ try:
187
+ return await call()
188
+ except (KeyError, TypeError, ValueError) as exc:
189
+ raise malformed(exc) from exc
190
+
191
+
192
+ def malformed(exc: Exception) -> InternetDataError:
193
+ return InternetDataError("server_error", f"malformed response from the API: {exc}")
194
+
195
+
196
+ def unwrap(res: Response[Any]) -> dict[str, Any]:
197
+ """The response body as it came off the wire, or the failure it describes."""
198
+ body = _decode(res.content)
199
+ status = int(res.status_code)
200
+ if not 200 <= status < 300:
201
+ raise error_from_response(status, _headers(res), body)
202
+ if not isinstance(body, dict):
203
+ raise InternetDataError("server_error", "the API answered with a non-object body", status)
204
+ return body
205
+
206
+
207
+ def as_error(exc: InternetDataError | httpx.HTTPError) -> InternetDataError:
208
+ """A transport failure, as the one error type this library raises.
209
+
210
+ Deliberately narrow: anything else is a bug rather than a failed request, and turning
211
+ it into a `network` error here would hide it behind a retry.
212
+ """
213
+ if isinstance(exc, InternetDataError):
214
+ return exc
215
+ return InternetDataError("network", str(exc) or type(exc).__name__)
216
+
217
+
218
+ def parse_body(body: dict[str, Any], parse: Callable[[dict[str, Any]], T]) -> T:
219
+ """A served body through the model layer, with a malformed one reported as the
220
+ server's failure rather than as a traceback out of a dataclass constructor."""
221
+ try:
222
+ return parse(body)
223
+ except (KeyError, TypeError, ValueError) as exc:
224
+ raise malformed(exc) from exc
225
+
226
+
227
+ def redirect_location(res: Response[Any]) -> str:
228
+ """Where a `302` points, or whatever the API said instead."""
229
+ location: str | None = _headers(res).get("location")
230
+ if int(res.status_code) == 302 and location:
231
+ return location
232
+ unwrap(res)
233
+ raise InternetDataError(
234
+ "server_error", "expected a redirect to object storage", int(res.status_code)
235
+ )
236
+
237
+
238
+ def databases_of(body: dict[str, Any]) -> list[Database]:
239
+ """Exactly the families the server listed for THIS key, in the order it listed them.
240
+
241
+ Nothing is added here, and nothing may be: a family built for a single customer is
242
+ absent from this array for every organization that does not license it, so filling a
243
+ gap from any other source would publish the fact that the family exists.
244
+ """
245
+ return [to_database(d) for d in body["databases"]]
246
+
247
+
248
+ def downloads_of(body: dict[str, Any]) -> list[Download]:
249
+ return [to_download(d) for d in body["downloads"]]
250
+
251
+
252
+ def checksums_of(body: dict[str, Any]) -> dict[str, str]:
253
+ """The digests, unwrapped one level past the envelope.
254
+
255
+ The response carries `id` and `format` beside them, so reading a top-level `sha256`
256
+ finds nothing. That exact mistake shipped in another binding's 1.0.x, which is why
257
+ the shared corpus pins the depth.
258
+ """
259
+ return dict(body["checksums"])
260
+
261
+
262
+ def retry_delay(err: InternetDataError, attempt: int, retries: int) -> float | None:
263
+ """How long to wait before attempt `attempt + 1`, or None when there must not be one.
264
+
265
+ A server-supplied `Retry-After` wins over the backoff schedule outright: it is the
266
+ only thing that makes a 429 worth retrying at all, so second-guessing it with a
267
+ shorter wait would just spend the next attempt on the same rejection.
268
+ """
269
+ if attempt >= retries or not err.retryable:
270
+ return None
271
+ if err.retry_after_seconds is not None:
272
+ return err.retry_after_seconds
273
+ return _BACKOFF_BASE * (2.0**attempt)
274
+
275
+
276
+ # The generated Response declares a plain MutableMapping, but always carries httpx's
277
+ # case-insensitive Headers. Rebuilding one keeps a header lookup case-blind whichever it
278
+ # turns out to be, which matters for `Retry-After`.
279
+ def _headers(res: Response[Any]) -> httpx.Headers:
280
+ return httpx.Headers(res.headers)
281
+
282
+
283
+ def _decode(content: bytes) -> Any:
284
+ try:
285
+ return json.loads(content)
286
+ except ValueError:
287
+ return None
@@ -0,0 +1,8 @@
1
+ """A client library for accessing InternetData API"""
2
+
3
+ from .client import AuthenticatedClient, Client
4
+
5
+ __all__ = (
6
+ "AuthenticatedClient",
7
+ "Client",
8
+ )
@@ -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_v2_format import DatabaseChecksumV2Format
9
+ from ...models.database_checksum_v2_response_200 import DatabaseChecksumV2Response200
10
+ from ...models.error import Error
11
+ from ...types import UNSET, Response
12
+
13
+
14
+ def _get_kwargs(
15
+ *,
16
+ id: str,
17
+ format_: DatabaseChecksumV2Format,
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/v2/database/checksum",
32
+ "params": params,
33
+ }
34
+
35
+ return _kwargs
36
+
37
+
38
+ def _parse_response(
39
+ *, client: AuthenticatedClient | Client, response: httpx.Response
40
+ ) -> DatabaseChecksumV2Response200 | Error | None:
41
+ if response.status_code == 200:
42
+ response_200 = DatabaseChecksumV2Response200.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[DatabaseChecksumV2Response200 | 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_: DatabaseChecksumV2Format,
93
+ ) -> Response[DatabaseChecksumV2Response200 | Error]:
94
+ """Checksums for one published file, to verify a download
95
+
96
+ Args:
97
+ id (str): Example: vpn_ip_v1.
98
+ format_ (DatabaseChecksumV2Format): Example: mmdb.
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[DatabaseChecksumV2Response200 | 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_: DatabaseChecksumV2Format,
125
+ ) -> DatabaseChecksumV2Response200 | Error | None:
126
+ """Checksums for one published file, to verify a download
127
+
128
+ Args:
129
+ id (str): Example: vpn_ip_v1.
130
+ format_ (DatabaseChecksumV2Format): Example: mmdb.
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
+ DatabaseChecksumV2Response200 | 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_: DatabaseChecksumV2Format,
152
+ ) -> Response[DatabaseChecksumV2Response200 | Error]:
153
+ """Checksums for one published file, to verify a download
154
+
155
+ Args:
156
+ id (str): Example: vpn_ip_v1.
157
+ format_ (DatabaseChecksumV2Format): Example: mmdb.
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[DatabaseChecksumV2Response200 | 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_: DatabaseChecksumV2Format,
182
+ ) -> DatabaseChecksumV2Response200 | Error | None:
183
+ """Checksums for one published file, to verify a download
184
+
185
+ Args:
186
+ id (str): Example: vpn_ip_v1.
187
+ format_ (DatabaseChecksumV2Format): Example: mmdb.
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
+ DatabaseChecksumV2Response200 | Error
195
+ """
196
+
197
+ return (
198
+ await asyncio_detailed(
199
+ client=client,
200
+ id=id,
201
+ format_=format_,
202
+ )
203
+ ).parsed