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.
- internetdata/__init__.py +49 -0
- internetdata/_core.py +287 -0
- internetdata/_generated/__init__.py +8 -0
- internetdata/_generated/api/__init__.py +1 -0
- internetdata/_generated/api/database_v_2/__init__.py +1 -0
- internetdata/_generated/api/database_v_2/database_checksum_v2.py +203 -0
- internetdata/_generated/api/database_v_2/database_metadata_v2.py +205 -0
- internetdata/_generated/api/database_v_2/download_database_v2.py +213 -0
- internetdata/_generated/api/database_v_2/list_databases.py +162 -0
- internetdata/_generated/api/database_v_2/list_downloads.py +178 -0
- internetdata/_generated/client.py +272 -0
- internetdata/_generated/errors.py +16 -0
- internetdata/_generated/models/__init__.py +53 -0
- internetdata/_generated/models/database.py +233 -0
- internetdata/_generated/models/database_checksum_v2_format.py +9 -0
- internetdata/_generated/models/database_checksum_v2_response_200.py +85 -0
- internetdata/_generated/models/database_checksum_v2_response_200_format.py +9 -0
- internetdata/_generated/models/database_metadata.py +132 -0
- internetdata/_generated/models/database_metadata_column.py +80 -0
- internetdata/_generated/models/database_metadata_sample.py +78 -0
- internetdata/_generated/models/database_metadata_sample_additional_property_item.py +45 -0
- internetdata/_generated/models/database_metadata_schema.py +72 -0
- internetdata/_generated/models/database_metadata_size.py +47 -0
- internetdata/_generated/models/database_redistribution_type_1.py +10 -0
- internetdata/_generated/models/database_redistribution_type_2_type_1.py +10 -0
- internetdata/_generated/models/database_redistribution_type_3_type_1.py +10 -0
- internetdata/_generated/models/database_standing.py +10 -0
- internetdata/_generated/models/database_version.py +97 -0
- internetdata/_generated/models/database_version_formats_item.py +9 -0
- internetdata/_generated/models/db_checksums.py +85 -0
- internetdata/_generated/models/download.py +161 -0
- internetdata/_generated/models/download_database_v2_format.py +9 -0
- internetdata/_generated/models/download_outcome.py +13 -0
- internetdata/_generated/models/error.py +62 -0
- internetdata/_generated/models/list_databases_response_200.py +75 -0
- internetdata/_generated/models/list_downloads_response_200.py +75 -0
- internetdata/_generated/types.py +54 -0
- internetdata/aio.py +268 -0
- internetdata/client.py +278 -0
- internetdata/errors.py +128 -0
- internetdata/models.py +197 -0
- internetdata/py.typed +0 -0
- internetdata-1.0.0.dist-info/METADATA +184 -0
- internetdata-1.0.0.dist-info/RECORD +46 -0
- internetdata-1.0.0.dist-info/WHEEL +4 -0
- internetdata-1.0.0.dist-info/licenses/LICENSE +21 -0
internetdata/errors.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""The one exception this library raises, and how a response becomes one."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import email.utils
|
|
7
|
+
import math
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
__all__ = ["ErrorKind", "InternetDataError"]
|
|
13
|
+
|
|
14
|
+
ErrorKind = Literal[
|
|
15
|
+
"bad_request",
|
|
16
|
+
"unauthorized",
|
|
17
|
+
"forbidden",
|
|
18
|
+
"rate_limited",
|
|
19
|
+
"quota_exceeded",
|
|
20
|
+
"server_error",
|
|
21
|
+
"network",
|
|
22
|
+
]
|
|
23
|
+
"""Why a request failed.
|
|
24
|
+
|
|
25
|
+
`rate_limited` and `quota_exceeded` both arrive as HTTP 429 and are NOT the same thing.
|
|
26
|
+
A rate limit is the API protecting itself and carries `Retry-After`; retrying works. A
|
|
27
|
+
spent quota carries no such header and retrying will not help until the window rolls
|
|
28
|
+
over or the limit is raised. The header is the only thing that distinguishes them.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_RETRYABLE: frozenset[str] = frozenset({"rate_limited", "server_error", "network"})
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class InternetDataError(Exception):
|
|
35
|
+
"""Every failure this library reports, discriminated by `kind`."""
|
|
36
|
+
|
|
37
|
+
kind: ErrorKind
|
|
38
|
+
status: int | None
|
|
39
|
+
retry_after_seconds: float | None
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
kind: ErrorKind,
|
|
44
|
+
message: str,
|
|
45
|
+
status: int | None = None,
|
|
46
|
+
retry_after_seconds: float | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
super().__init__(message)
|
|
49
|
+
self.kind = kind
|
|
50
|
+
self.status = status
|
|
51
|
+
self.retry_after_seconds = retry_after_seconds
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def message(self) -> str:
|
|
55
|
+
return str(self)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def retryable(self) -> bool:
|
|
59
|
+
"""Whether retrying this exact request could succeed."""
|
|
60
|
+
return self.kind in _RETRYABLE
|
|
61
|
+
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
return (
|
|
64
|
+
f"InternetDataError(kind={self.kind!r}, status={self.status!r}, message={str(self)!r})"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def error_from_response(status: int, headers: httpx.Headers, body: Any) -> InternetDataError:
|
|
69
|
+
"""Classify a failed response.
|
|
70
|
+
|
|
71
|
+
Everything outside the four named statuses is decided by its RANGE rather than by an
|
|
72
|
+
enumerated list, which is the whole point: a 404 from a database id that does not
|
|
73
|
+
exist has to land on a client error, and mapping 400/401/403/429 and letting the rest
|
|
74
|
+
fall through to `server_error` retries it twice before failing. Three of the four
|
|
75
|
+
first SDKs shipped exactly that bug, so the shared corpus pins both 404 shapes.
|
|
76
|
+
"""
|
|
77
|
+
message = _message_of(body) or f"request failed with status {status}"
|
|
78
|
+
retry_after = _parse_retry_after(headers.get("retry-after"))
|
|
79
|
+
|
|
80
|
+
if status == 429:
|
|
81
|
+
# Present means transient, absent means an allowance is spent. Nothing else in
|
|
82
|
+
# the response separates the two.
|
|
83
|
+
if retry_after is None:
|
|
84
|
+
return InternetDataError("quota_exceeded", message, status)
|
|
85
|
+
return InternetDataError("rate_limited", message, status, retry_after)
|
|
86
|
+
if status == 401:
|
|
87
|
+
return InternetDataError("unauthorized", message, status)
|
|
88
|
+
if status == 403:
|
|
89
|
+
return InternetDataError("forbidden", message, status)
|
|
90
|
+
if 400 <= status < 500:
|
|
91
|
+
return InternetDataError("bad_request", message, status)
|
|
92
|
+
return InternetDataError("server_error", message, status)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _message_of(body: Any) -> str | None:
|
|
96
|
+
"""The API's own result code, which is the whole error envelope.
|
|
97
|
+
|
|
98
|
+
`rc` is deliberately not an enum in the spec, so a code added later stays readable by
|
|
99
|
+
a client generated today. Passing it through verbatim is what keeps that promise: a
|
|
100
|
+
caller switching on `NOT_LICENSED` versus `LICENSE_EXPIRED` needs the string, not a
|
|
101
|
+
status.
|
|
102
|
+
"""
|
|
103
|
+
if not isinstance(body, dict):
|
|
104
|
+
return None
|
|
105
|
+
code = body.get("rc")
|
|
106
|
+
if isinstance(code, str) and code != "":
|
|
107
|
+
return code
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _parse_retry_after(value: str | None) -> float | None:
|
|
112
|
+
if value is None or value.strip() == "":
|
|
113
|
+
return None
|
|
114
|
+
try:
|
|
115
|
+
seconds = float(value)
|
|
116
|
+
except ValueError:
|
|
117
|
+
pass
|
|
118
|
+
else:
|
|
119
|
+
return seconds if seconds >= 0 else None
|
|
120
|
+
# The header also permits an HTTP date.
|
|
121
|
+
try:
|
|
122
|
+
when = email.utils.parsedate_to_datetime(value)
|
|
123
|
+
except (TypeError, ValueError):
|
|
124
|
+
return None
|
|
125
|
+
if when.tzinfo is None:
|
|
126
|
+
when = when.replace(tzinfo=datetime.UTC)
|
|
127
|
+
delta = (when - datetime.datetime.now(datetime.UTC)).total_seconds()
|
|
128
|
+
return max(0.0, math.ceil(delta))
|
internetdata/models.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""What the API answers, and the one place the wire shape becomes an idiomatic one.
|
|
2
|
+
|
|
3
|
+
The generated models are a wire contract rather than an API: the spec spells
|
|
4
|
+
`redistribution` as a nullable enum, which the generator renders as a union of three
|
|
5
|
+
single-member enum classes, and nobody should have to read that. These are frozen
|
|
6
|
+
dataclasses of plain values, built straight from the served JSON, with `raw` kept beside
|
|
7
|
+
them so a field this pinned spec predates is still reachable.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import datetime
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Database",
|
|
18
|
+
"DatabaseMetadata",
|
|
19
|
+
"DatabaseVersion",
|
|
20
|
+
"Download",
|
|
21
|
+
"Format",
|
|
22
|
+
"MetadataColumn",
|
|
23
|
+
"Outcome",
|
|
24
|
+
"Redistribution",
|
|
25
|
+
"Standing",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
Format = Literal["csvgz", "mmdb"]
|
|
29
|
+
"""The file formats a database version can be built in.
|
|
30
|
+
|
|
31
|
+
Which of them a given version HAS is `DatabaseVersion.formats`; asking for another is a
|
|
32
|
+
`bad_request` rather than a gap, because the `_provider` catalogs are keyed by provider
|
|
33
|
+
id and no MMDB exists for them.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
Standing = Literal["licensed", "expired", "unlicensed"]
|
|
37
|
+
"""Where your organization stands with one database family."""
|
|
38
|
+
|
|
39
|
+
Redistribution = Literal["evaluation", "internal", "redistribute"]
|
|
40
|
+
"""What a licence permits you to do with the data. `None` when there is no licence."""
|
|
41
|
+
|
|
42
|
+
Outcome = Literal["ok", "unauthorized", "denied", "expired", "unknown", "unavailable"]
|
|
43
|
+
"""How one download attempt ended, refusals included."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class DatabaseVersion:
|
|
48
|
+
"""One published version of a family.
|
|
49
|
+
|
|
50
|
+
Old versions are frozen rather than migrated, so several stay downloadable at once.
|
|
51
|
+
`id` is what `download`, `checksums` and `metadata` take; the family `base` is what a
|
|
52
|
+
licence is held against.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
id: str
|
|
56
|
+
version: int
|
|
57
|
+
summary: str
|
|
58
|
+
formats: tuple[Format, ...]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class Database:
|
|
63
|
+
"""One database FAMILY, with your organization's licence beside it.
|
|
64
|
+
|
|
65
|
+
A family your organization has never licensed is still listed, with `standing` set to
|
|
66
|
+
`unlicensed`, so you can see what else exists. A family commissioned for a single
|
|
67
|
+
customer is a different matter: it is absent from this listing entirely for everyone
|
|
68
|
+
who does not license it. Absence here means "not yours to see", never "does not
|
|
69
|
+
exist", so the catalog is not the same document for every key.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
base: str
|
|
73
|
+
name: str
|
|
74
|
+
summary: str
|
|
75
|
+
standing: Standing
|
|
76
|
+
redistribution: Redistribution | None
|
|
77
|
+
starts: datetime.datetime | None
|
|
78
|
+
expires: datetime.datetime | None
|
|
79
|
+
versions: tuple[DatabaseVersion, ...]
|
|
80
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class MetadataColumn:
|
|
85
|
+
"""One column of a published file. `description` is absent on older builds."""
|
|
86
|
+
|
|
87
|
+
name: str
|
|
88
|
+
type: str
|
|
89
|
+
description: str | None = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class DatabaseMetadata:
|
|
94
|
+
"""What is inside one database: freshness, row count, columns, samples and sizes.
|
|
95
|
+
|
|
96
|
+
Poll this to decide whether today's build is worth fetching. `size` is keyed by
|
|
97
|
+
format and is in bytes, which is what a caller should budget a transfer against
|
|
98
|
+
before starting one.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
id: str
|
|
102
|
+
updated: datetime.date
|
|
103
|
+
entries: int
|
|
104
|
+
schema: dict[str, tuple[MetadataColumn, ...]]
|
|
105
|
+
size: dict[str, int]
|
|
106
|
+
update_freq: str | None = None
|
|
107
|
+
sample: dict[str, tuple[dict[str, Any], ...]] = field(default_factory=dict)
|
|
108
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class Download:
|
|
113
|
+
"""One download ATTEMPT by your organization, refusals included.
|
|
114
|
+
|
|
115
|
+
`bytes` is the object size when the link was minted rather than bytes delivered: the
|
|
116
|
+
transfer is a presigned redirect straight to object storage, so the API never
|
|
117
|
+
observes how much of it was taken.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
dataset_id: str
|
|
121
|
+
format: str
|
|
122
|
+
outcome: Outcome
|
|
123
|
+
created: datetime.datetime
|
|
124
|
+
bytes: int | None = None
|
|
125
|
+
http_status: int | None = None
|
|
126
|
+
apikey_id: str | None = None
|
|
127
|
+
client_ip: str | None = None
|
|
128
|
+
user_agent: str | None = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def to_database(body: dict[str, Any]) -> Database:
|
|
132
|
+
return Database(
|
|
133
|
+
base=body["base"],
|
|
134
|
+
name=body["name"],
|
|
135
|
+
summary=body["summary"],
|
|
136
|
+
standing=body["standing"],
|
|
137
|
+
redistribution=body["redistribution"],
|
|
138
|
+
starts=_datetime(body["starts"]),
|
|
139
|
+
expires=_datetime(body["expires"]),
|
|
140
|
+
versions=tuple(to_version(v) for v in body["versions"]),
|
|
141
|
+
raw=body,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def to_version(body: dict[str, Any]) -> DatabaseVersion:
|
|
146
|
+
return DatabaseVersion(
|
|
147
|
+
id=body["id"],
|
|
148
|
+
version=body["version"],
|
|
149
|
+
summary=body["summary"],
|
|
150
|
+
formats=tuple(body["formats"]),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def to_metadata(body: dict[str, Any]) -> DatabaseMetadata:
|
|
155
|
+
return DatabaseMetadata(
|
|
156
|
+
id=body["id"],
|
|
157
|
+
updated=datetime.date.fromisoformat(body["updated"]),
|
|
158
|
+
entries=body["entries"],
|
|
159
|
+
schema={
|
|
160
|
+
fmt: tuple(to_column(c) for c in columns) for fmt, columns in body["schema"].items()
|
|
161
|
+
},
|
|
162
|
+
size=dict(body["size"]),
|
|
163
|
+
update_freq=body.get("update_freq"),
|
|
164
|
+
sample={fmt: tuple(rows) for fmt, rows in body.get("sample", {}).items()},
|
|
165
|
+
raw=body,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def to_column(body: dict[str, Any]) -> MetadataColumn:
|
|
170
|
+
return MetadataColumn(name=body["name"], type=body["type"], description=body.get("description"))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def to_download(body: dict[str, Any]) -> Download:
|
|
174
|
+
return Download(
|
|
175
|
+
dataset_id=body["dataset_id"],
|
|
176
|
+
format=body["format"],
|
|
177
|
+
outcome=body["outcome"],
|
|
178
|
+
created=datetime.datetime.fromisoformat(body["created"]),
|
|
179
|
+
bytes=body["bytes"],
|
|
180
|
+
http_status=body["http_status"],
|
|
181
|
+
apikey_id=body["apikey_id"],
|
|
182
|
+
client_ip=body["client_ip"],
|
|
183
|
+
user_agent=body["user_agent"],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _datetime(value: Any) -> datetime.datetime | None:
|
|
188
|
+
"""A NULLABLE wire timestamp. A licence with no end date carries `expires: null`.
|
|
189
|
+
|
|
190
|
+
`fromisoformat` only learned to read a trailing `Z` in 3.11, which is one of the
|
|
191
|
+
three reasons this package floors there. A value that is neither null nor readable
|
|
192
|
+
raises, and the caller reports it as the server's failure rather than as None, which
|
|
193
|
+
would quietly read as "no end date".
|
|
194
|
+
"""
|
|
195
|
+
if value is None:
|
|
196
|
+
return None
|
|
197
|
+
return datetime.datetime.fromisoformat(value)
|
internetdata/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: internetdata
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client library for the InternetData API. Download and verify licensed IP datasets.
|
|
5
|
+
Project-URL: Homepage, https://internetdata.io
|
|
6
|
+
Project-URL: Documentation, https://docs.internetdata.io
|
|
7
|
+
Project-URL: Source, https://github.com/internetdata/sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/internetdata/sdk-python/issues
|
|
9
|
+
Author-email: Mslm Dev <support@internetdata.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: asn,bogon,dataset,geolocation,hosting,internetdata,ip-database,ip-intelligence,mmdb,proxy,vpn
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
|
+
Classifier: Topic :: Internet
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: attrs>=22.2.0
|
|
26
|
+
Requires-Dist: httpx<1,>=0.27
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
29
|
+
Requires-Dist: openapi-python-client<0.30,>=0.29; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# [<img src="https://s3.internetdata.io/internetdata-public/brand/mark.svg" alt="InternetData" width="24"/>](https://internetdata.io/) InternetData Python Client Library
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/internetdata/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
|
|
40
|
+
The official Python client library for the [InternetData](https://internetdata.io) API.
|
|
41
|
+
|
|
42
|
+
The library helps you browse the databases your organization is licensed for, check what is in each build before you fetch it, and download and verify the files.
|
|
43
|
+
|
|
44
|
+
## Getting Started
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install internetdata
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Requires Python 3.11 or newer. Type hints are included, and the package ships `py.typed`.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
Every call needs an API key carrying the `db.download` scope. Create one in the console, then:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import os
|
|
58
|
+
from internetdata import InternetData
|
|
59
|
+
|
|
60
|
+
client = InternetData(os.environ["INTERNETDATA_API_KEY"])
|
|
61
|
+
|
|
62
|
+
for family in client.database.list():
|
|
63
|
+
print(family.base, family.standing, [v.id for v in family.versions])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The client holds an HTTP connection pool, so use it as a context manager, or call `client.close()` when you are done with it:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
with InternetData(api_key) as client:
|
|
70
|
+
print(len(client.database.list()))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### The catalog
|
|
74
|
+
|
|
75
|
+
`list` answers database FAMILIES. A licence 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
|
+
|
|
77
|
+
```python
|
|
78
|
+
for family in client.database.list():
|
|
79
|
+
if family.standing != "licensed":
|
|
80
|
+
continue
|
|
81
|
+
for version in family.versions:
|
|
82
|
+
print(version.id, version.formats) # 'bogon_ip_v1' ('csvgz', 'mmdb')
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`standing` is `licensed`, `expired` or `unlicensed`, and `redistribution` is what your licence lets you do with the data (`evaluation`, `internal`, `redistribute`, or `None` when there is no licence). A family you have never bought is still listed, as `unlicensed`, so you can see what else exists.
|
|
86
|
+
|
|
87
|
+
**The catalog is not the same document for every key.** Databases commissioned for a single customer are absent from the listing entirely for everyone else, rather than shown as `unlicensed`. The server decides that per key, so treat what you get back as the answer for the key you asked with: this library never caches a listing, never reuses one across clients, and has no built-in list of database ids to fall back on.
|
|
88
|
+
|
|
89
|
+
### What is inside a build
|
|
90
|
+
|
|
91
|
+
`metadata` is cheap enough to poll. It answers when the build was generated, how many rows it has, its columns and a few real rows, and the size of each format in bytes, all without moving the file:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
meta = client.database.metadata("bogon_ip_v1")
|
|
95
|
+
|
|
96
|
+
print(meta.updated) # datetime.date(2026, 9, 4)
|
|
97
|
+
print(meta.entries) # 1234
|
|
98
|
+
print(meta.size["csvgz"]) # 760
|
|
99
|
+
print([c.name for c in meta.schema["csvgz"]])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Checking `size` before a download is worth the round trip: the catalog spans a few hundred bytes to several gigabytes.
|
|
103
|
+
|
|
104
|
+
### Downloading
|
|
105
|
+
|
|
106
|
+
Three ways, depending on what you want to do with the bytes:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
url = client.database.download_url("bogon_ip_v1", "csvgz")
|
|
110
|
+
raw = client.database.download_bytes("bogon_ip_v1", "csvgz")
|
|
111
|
+
written = client.database.download("bogon_ip_v1", "mmdb", "./bogon_ip_v1.mmdb")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`download_url` hands back a time-limited link straight to object storage. It carries its own signature and none of your API key, so you can pass it to `curl`, a job runner or anything else that speaks HTTP. The link authorizes the START of a transfer, so one already running is not cut off when it lapses.
|
|
115
|
+
|
|
116
|
+
`download` streams to a file, so nothing larger than a chunk is ever held in memory. The bytes land in a neighboring `.part` file that is moved into place only once the whole transfer has arrived, so a failed refresh leaves the copy you already had untouched. `download_bytes` holds the whole file in memory, so reach for it only at the small end of the catalog.
|
|
117
|
+
|
|
118
|
+
Either way a short transfer fails rather than handing you a truncated file, and the key is never sent to object storage.
|
|
119
|
+
|
|
120
|
+
Verify what you fetched against the published digests:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
import hashlib
|
|
124
|
+
|
|
125
|
+
digests = client.database.checksums("bogon_ip_v1", "csvgz")
|
|
126
|
+
assert hashlib.sha256(raw).hexdigest() == digests["sha256"]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Download history
|
|
130
|
+
|
|
131
|
+
`downloads` answers your organization's recent attempts, newest first, refusals included. A denial is what answers "it stopped working", and its absence answers nothing:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
for attempt in client.database.downloads(limit=20):
|
|
135
|
+
print(attempt.created, attempt.dataset_id, attempt.outcome, attempt.http_status)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Async
|
|
139
|
+
|
|
140
|
+
Everything above works the same way under asyncio, with `AsyncInternetData`:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import asyncio
|
|
144
|
+
from internetdata import AsyncInternetData
|
|
145
|
+
|
|
146
|
+
async def main():
|
|
147
|
+
async with AsyncInternetData(api_key) as client:
|
|
148
|
+
meta = await client.database.metadata("bogon_ip_v1")
|
|
149
|
+
await client.database.download("bogon_ip_v1", "csvgz", "./bogon_ip_v1.csv.gz")
|
|
150
|
+
print(meta.entries)
|
|
151
|
+
|
|
152
|
+
asyncio.run(main())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Errors
|
|
156
|
+
|
|
157
|
+
Failures raise an `InternetDataError` carrying a `kind` and a `retryable` flag:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from internetdata import InternetDataError
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
client.database.download_url("vpn_ip_v1", "mmdb")
|
|
164
|
+
except InternetDataError as err:
|
|
165
|
+
print(err.kind, err.retryable, err.message) # forbidden False NOT_LICENSED
|
|
166
|
+
```
|
|
167
|
+
|
|
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.
|
|
169
|
+
|
|
170
|
+
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.
|
|
171
|
+
|
|
172
|
+
## Other Libraries
|
|
173
|
+
|
|
174
|
+
There are official InternetData client libraries available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. See our GitHub at https://github.com/internetdata for more.
|
|
175
|
+
|
|
176
|
+
## About InternetData
|
|
177
|
+
|
|
178
|
+
InternetData: IP and network intelligence databases, published as CSV and MMDB, licensed by contract and downloadable on demand.
|
|
179
|
+
|
|
180
|
+
[<img src="https://s3.internetdata.io/internetdata-public/brand/mark.svg" alt="InternetData" width="96"/>](https://internetdata.io/)
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
internetdata/__init__.py,sha256=RxzMbXKap5PrHulNUwHJSLGkrIM1mMcD88CR1ShEWiY,1105
|
|
2
|
+
internetdata/_core.py,sha256=3waAQnkS_0KYaSl-7bHmBDjwOiR-R0A0z6G0oi8jnKU,11064
|
|
3
|
+
internetdata/aio.py,sha256=KF-8PX7MQYIDFupvzyI7LUkP4vHl9AljdHAFDFzyVww,10311
|
|
4
|
+
internetdata/client.py,sha256=42dL-9pyLetDFpkDbOypWkUBjsS4TjRn2zd-uwMMlOc,10379
|
|
5
|
+
internetdata/errors.py,sha256=dYmFTImvgNqNPgIrcPCw2fGTZf8c_Ws7Clu8otiDLxw,4316
|
|
6
|
+
internetdata/models.py,sha256=M-w6AyqV7l1uM9FsRHkGsc_bXN2TGxE5Me6ylp2nNdY,6495
|
|
7
|
+
internetdata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
internetdata/_generated/__init__.py,sha256=wqKoc5J-jEpNI_y90nHagEpn2sTWbwTYnMmCgqDEBq0,159
|
|
9
|
+
internetdata/_generated/client.py,sha256=MHk8TC1YjyaLkvQ3tF0HBP561cJZmQY8Ih6seOCcOMI,12382
|
|
10
|
+
internetdata/_generated/errors.py,sha256=gO8GBmKqmSNgAg-E5oT-oOyxztvp7V_6XG7OUTT15q0,546
|
|
11
|
+
internetdata/_generated/types.py,sha256=0We4NPvhIYASRpQ3le41nmJeEAVm42-2VKdzlJ4Ogok,1343
|
|
12
|
+
internetdata/_generated/api/__init__.py,sha256=zTSiG_ujSjAqWPyc435YXaX9XTlpMjiJWBbV-f-YtdA,45
|
|
13
|
+
internetdata/_generated/api/database_v_2/__init__.py,sha256=5vd9uJWAjRqa9xzxzYkLD1yoZ12Ld_bAaNB5WX4fbE8,56
|
|
14
|
+
internetdata/_generated/api/database_v_2/database_checksum_v2.py,sha256=MI-zxBNO8y-_ZGJ8zNbnv_eIrMgrGyHxeIoZ18w3Ir0,5448
|
|
15
|
+
internetdata/_generated/api/database_v_2/database_metadata_v2.py,sha256=WVv8-Qe_rGtkvLxyOb50_HcrwixB3z6PqlzOaPX0rFI,5446
|
|
16
|
+
internetdata/_generated/api/database_v_2/download_database_v2.py,sha256=Qf_PDFwAn1rHxsI3P9Db2KhUoekIN6ik_Vn1t5uCPlk,5554
|
|
17
|
+
internetdata/_generated/api/database_v_2/list_databases.py,sha256=u8hDrKC2BEdKuh-a9p4jKxVBKfzlQD7ufAGf5j52uJ8,4958
|
|
18
|
+
internetdata/_generated/api/database_v_2/list_downloads.py,sha256=-2WV7ghS4e5On8xPRzAuHAHBmhkl83rWYZEVyJy5SaI,4820
|
|
19
|
+
internetdata/_generated/models/__init__.py,sha256=KyWrvMFBzQIxvajNAth3TEIQXOTPhtOKgmRNbmOwo1g,2170
|
|
20
|
+
internetdata/_generated/models/database.py,sha256=v_zZZ_9EUbNXRoJNstk8H_uX8VTIPJr__uQElLCVqG4,7983
|
|
21
|
+
internetdata/_generated/models/database_checksum_v2_format.py,sha256=nof7_Q3xI2VhEN3GzhqUc-nGqEp5UiyUQSVGKqOqDSo,168
|
|
22
|
+
internetdata/_generated/models/database_checksum_v2_response_200.py,sha256=bw21224pDJtt28sQNuddVCv4gA57DmqzBjU6-5O9mow,2311
|
|
23
|
+
internetdata/_generated/models/database_checksum_v2_response_200_format.py,sha256=PIn4sLd4drgU1Y90enf3NC39Z4acdH8WRZ85ohdPOS0,179
|
|
24
|
+
internetdata/_generated/models/database_metadata.py,sha256=yMjTI9YHiQ61E4F9tzAFciRjcANy494mVf-yaFrOA4k,4039
|
|
25
|
+
internetdata/_generated/models/database_metadata_column.py,sha256=7EaGGmKkeFTSdUvsNKGuVJRx_gwQQZ63dapiJNhpm3A,2005
|
|
26
|
+
internetdata/_generated/models/database_metadata_sample.py,sha256=LaS_rGlKTljIItUN9giTM-HXUAG3mnjV1iat51BtQUM,2642
|
|
27
|
+
internetdata/_generated/models/database_metadata_sample_additional_property_item.py,sha256=fapR9XWubK3WS07JfgKrnbQbLQN1OOagR7pnTt8TM_Q,1401
|
|
28
|
+
internetdata/_generated/models/database_metadata_schema.py,sha256=DiWZkwkbJr3KqseZbq0837GuLsn9QzZe6in74M0Uhqk,2394
|
|
29
|
+
internetdata/_generated/models/database_metadata_size.py,sha256=yZ9lYIK2fM3npNC6Cz10l8Gw1CfvF12rSA_GWRFH8SU,1301
|
|
30
|
+
internetdata/_generated/models/database_redistribution_type_1.py,sha256=4XI7wpGnmbQ52EGB-bTlOXD1_G5ubbq0kXkGSRBXIJo,223
|
|
31
|
+
internetdata/_generated/models/database_redistribution_type_2_type_1.py,sha256=3eHBM6r-0F8epfzr14yq8hpncuqY1NjA9wWaRl4mAIw,228
|
|
32
|
+
internetdata/_generated/models/database_redistribution_type_3_type_1.py,sha256=h3eqMgSTEEM47Q-ATEqueDLcsAtzKRJGy1l18XKwfZI,228
|
|
33
|
+
internetdata/_generated/models/database_standing.py,sha256=fOhY--mUbtkr188IySudfuyub5XpjnNjudrQ93MRUvU,202
|
|
34
|
+
internetdata/_generated/models/database_version.py,sha256=cqkfPCiY4BBQIiC5sAtFZp75RSkTctlFS1dbWqeki_Y,2723
|
|
35
|
+
internetdata/_generated/models/database_version_formats_item.py,sha256=IGjztCYren6snoX0zb-m0KTtwzNKGBmfBA4EZ3aJ_jc,170
|
|
36
|
+
internetdata/_generated/models/db_checksums.py,sha256=4hB_YuDm7gVqhyJB2h-YuYjRqxyI9Jq3Dw2RJEP-NFo,2038
|
|
37
|
+
internetdata/_generated/models/download.py,sha256=REGcSs5HF3atMI-vocxd2cY6-y-aoEExnYJhnc6BFdk,4586
|
|
38
|
+
internetdata/_generated/models/download_database_v2_format.py,sha256=XRhb_e2-eJwtQ6mJJ0qzqi3T2kYBFgpbWCAh8fF8Egc,168
|
|
39
|
+
internetdata/_generated/models/download_outcome.py,sha256=C_xdtqtpFbgg3M2WdT2nE48NPhErO4WMbqAx7_ZPJUA,271
|
|
40
|
+
internetdata/_generated/models/error.py,sha256=n6JO5D33IFfQ7UoB5LANoJzKdVGfdO0nhwra1uNJFaY,1558
|
|
41
|
+
internetdata/_generated/models/list_databases_response_200.py,sha256=c_qDy3Nq0Ymr2o8J6wsifvm_lWlnwtcZxGEJ86Q3Dhs,2071
|
|
42
|
+
internetdata/_generated/models/list_downloads_response_200.py,sha256=HWd_62XgkIAnNbnJ8Kft-D2Et6qaZWe64QCHkS7tMuU,2071
|
|
43
|
+
internetdata-1.0.0.dist-info/METADATA,sha256=bCspIg9ZfRhKtueBjhnxmBAzN-qBWpbYen71ljeZQGU,8224
|
|
44
|
+
internetdata-1.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
45
|
+
internetdata-1.0.0.dist-info/licenses/LICENSE,sha256=wglb3Uww0HrX4-4rYO9Xe4j8Sn9STPtK2M9Tt5NKTrQ,1065
|
|
46
|
+
internetdata-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mslm Dev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|