vpndetection 1.0.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.
- vpndetection-1.0.0/.gitignore +17 -0
- vpndetection-1.0.0/LICENSE +21 -0
- vpndetection-1.0.0/PKG-INFO +225 -0
- vpndetection-1.0.0/README.md +190 -0
- vpndetection-1.0.0/integration/scripts/run.py +147 -0
- vpndetection-1.0.0/integration/scripts/run.sh +26 -0
- vpndetection-1.0.0/pyproject.toml +95 -0
- vpndetection-1.0.0/scripts/download-spec.sh +18 -0
- vpndetection-1.0.0/scripts/generate.sh +47 -0
- vpndetection-1.0.0/scripts/publish.sh +47 -0
- vpndetection-1.0.0/spec/openapi.yaml +880 -0
- vpndetection-1.0.0/src/vpndetection/__init__.py +49 -0
- vpndetection-1.0.0/src/vpndetection/_bogons.py +63 -0
- vpndetection-1.0.0/src/vpndetection/_core.py +292 -0
- vpndetection-1.0.0/src/vpndetection/_generated/__init__.py +8 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/__init__.py +1 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/database/__init__.py +1 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/database/database_checksum.py +203 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/database/database_metadata.py +205 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/database/download_database.py +201 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/database/list_databases.py +134 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/database/list_downloads.py +166 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/lookup/__init__.py +1 -0
- vpndetection-1.0.0/src/vpndetection/_generated/api/lookup/lookup_ip.py +197 -0
- vpndetection-1.0.0/src/vpndetection/_generated/client.py +272 -0
- vpndetection-1.0.0/src/vpndetection/_generated/errors.py +16 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/__init__.py +63 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/class_detail.py +91 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/database_checksum_format.py +9 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/database_checksum_response_200.py +87 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/database_checksum_response_200_checksums.py +88 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_format_size.py +77 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_format_size_format.py +9 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata.py +167 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_column.py +80 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_sample.py +78 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_sample_additional_property_item.py +45 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_sample_size.py +47 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_schema.py +72 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_size.py +47 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/download.py +170 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/download_database_format.py +9 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/download_outcome.py +13 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/error.py +61 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_dataset.py +194 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_dataset_redistribution.py +10 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_dataset_standing.py +10 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_version.py +125 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_version_sample_formats_item.py +9 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/list_databases_response_200.py +75 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/list_downloads_response_200.py +75 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/lookup_error.py +62 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/lookup_response.py +295 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/proxy_detail.py +129 -0
- vpndetection-1.0.0/src/vpndetection/_generated/models/vpn_detail.py +101 -0
- vpndetection-1.0.0/src/vpndetection/_generated/types.py +54 -0
- vpndetection-1.0.0/src/vpndetection/aio.py +335 -0
- vpndetection-1.0.0/src/vpndetection/bogon.py +85 -0
- vpndetection-1.0.0/src/vpndetection/client.py +332 -0
- vpndetection-1.0.0/src/vpndetection/errors.py +119 -0
- vpndetection-1.0.0/src/vpndetection/models.py +190 -0
- vpndetection-1.0.0/src/vpndetection/py.typed +0 -0
- vpndetection-1.0.0/testdata/bogons.json +60 -0
- vpndetection-1.0.0/testdata/testdata.json +744 -0
- vpndetection-1.0.0/tests/conftest.py +16 -0
- vpndetection-1.0.0/tests/helpers.py +183 -0
- vpndetection-1.0.0/tests/test_client.py +141 -0
- vpndetection-1.0.0/tests/test_conformance.py +162 -0
- vpndetection-1.0.0/tests/test_download.py +275 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
|
|
4
|
+
# Build output.
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
|
|
9
|
+
# Virtualenvs, including any made to run codegen. Keep them out of the tree entirely.
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
.venv-*/
|
|
13
|
+
|
|
14
|
+
# Tool caches.
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
@@ -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.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vpndetection
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python client library for the VPNDetection API. Detect VPNs, proxies, Tor, hosting and CDN IPs.
|
|
5
|
+
Project-URL: Homepage, https://vpndetection.io
|
|
6
|
+
Project-URL: Documentation, https://docs.vpndetection.io
|
|
7
|
+
Project-URL: Source, https://github.com/vpndetection-io/sdk-python
|
|
8
|
+
Project-URL: Issues, https://github.com/vpndetection-io/sdk-python/issues
|
|
9
|
+
Author-email: Mslm Dev <support@vpndetection.io>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cdn,datacenter-proxy,fraud-prevention,hosting,ip-geolocation,ip-intelligence,mobile-proxy,proxy-detection,relay,residential-proxy,tor,vpn,vpn-detection
|
|
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: cachetools>=5.3
|
|
27
|
+
Requires-Dist: httpx<1,>=0.27
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
30
|
+
Requires-Dist: openapi-python-client<0.30,>=0.29; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="24"/>](https://vpndetection.io/) VPNDetection Python Client Library
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/vpndetection/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
The official Python client library for the [VPNDetection](https://vpndetection.io) API.
|
|
42
|
+
|
|
43
|
+
The library helps you query VPNDetection's APIs for anonymity detection including VPNs, residential proxies, Tor nodes, hosting servers, CDNs, relays and more.
|
|
44
|
+
|
|
45
|
+
## Getting Started
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install vpndetection
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Python 3.11 or newer. Type hints are included, and the package ships `py.typed`.
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
**No API key needed to start.** The free tier answers `ip` and `is_vpn`, and allows 1000 requests per day per source address.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from vpndetection import VPNDetection
|
|
59
|
+
|
|
60
|
+
client = VPNDetection()
|
|
61
|
+
|
|
62
|
+
result = client.lookup("45.83.91.1")
|
|
63
|
+
print(result.is_vpn) # True
|
|
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 VPNDetection() as client:
|
|
70
|
+
print(client.lookup("45.83.91.1").is_vpn)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### With an API key
|
|
74
|
+
|
|
75
|
+
An API key raises your quota, and raises your features on a paid plan. Create one in the [console](https://app.vpndetection.io), then pass it in:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import os
|
|
79
|
+
|
|
80
|
+
client = VPNDetection(os.environ["VPNDETECTION_API_KEY"])
|
|
81
|
+
|
|
82
|
+
result = client.lookup("45.83.91.1")
|
|
83
|
+
print(result.is_vpn) # True
|
|
84
|
+
print(result.vpn.provider) # 'mullvad'
|
|
85
|
+
print(result.is_hosting) # True
|
|
86
|
+
print(result.hosting.provider)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Async
|
|
90
|
+
|
|
91
|
+
Everything above works the same way under asyncio, with `AsyncVPNDetection`:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
import asyncio
|
|
95
|
+
from vpndetection import AsyncVPNDetection
|
|
96
|
+
|
|
97
|
+
async def main():
|
|
98
|
+
async with AsyncVPNDetection() as client:
|
|
99
|
+
result = await client.lookup("45.83.91.1")
|
|
100
|
+
print(result.is_vpn) # True
|
|
101
|
+
|
|
102
|
+
asyncio.run(main())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Batch lookup
|
|
106
|
+
|
|
107
|
+
You can do batch lookups with a list, which parallelizes requests for you efficiently:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
results = client.lookup_batch(["45.83.91.1", "8.8.8.8", "1.1.1.1"])
|
|
111
|
+
|
|
112
|
+
for ip, result in results.items():
|
|
113
|
+
if isinstance(result, Exception):
|
|
114
|
+
print(f"{ip}: {result}")
|
|
115
|
+
continue
|
|
116
|
+
print(f"{ip}: {result.is_vpn}")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Results are keyed by address, so duplicates in your list collapse into a single request and one address failing never loses the rest.
|
|
120
|
+
|
|
121
|
+
Concurrency and other variables are configurable per-call:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
results = client.lookup_batch(many_ips, concurrency=32, retries=4)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Caching
|
|
128
|
+
|
|
129
|
+
Answers are cached by default, so repeat lookups of the same address are free:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
client = VPNDetection()
|
|
133
|
+
|
|
134
|
+
result = client.lookup("45.83.91.1")
|
|
135
|
+
print(result.is_vpn) # True, API request
|
|
136
|
+
|
|
137
|
+
result2 = client.lookup("45.83.91.1")
|
|
138
|
+
print(result2.is_vpn) # True, no API request, result was cached
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
You can change the default cache variables (max size, TTL in seconds, etc) on initialization, or even disable it:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
client = VPNDetection(cache_max_size=50_000, cache_ttl=6 * 60 * 60)
|
|
145
|
+
client_no_cache = VPNDetection(cache=False)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Private and reserved addresses
|
|
149
|
+
|
|
150
|
+
Private, loopback, link-local, documentation and multicast addresses (and their IPv6 equivalents, including the 6to4 and Teredo ranges) can never be VPN or proxy infrastructure. The library answers them locally, so they cost no request and no quota:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
result = client.lookup("192.168.1.1")
|
|
154
|
+
result.is_bogon # True, this answer was computed rather than served
|
|
155
|
+
result.is_vpn # False
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The check is available on the client, which is handy when your inputs are addresses anyway:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
client.is_bogon("10.0.0.1") # True
|
|
162
|
+
client.is_bogon("8.8.8.8") # False
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
It is also importable on its own, if you want it without a client:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from vpndetection import is_bogon
|
|
169
|
+
|
|
170
|
+
is_bogon("10.0.0.1") # True
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Errors
|
|
174
|
+
|
|
175
|
+
Failures raise a `VPNDetectionError` carrying a `kind` and a `retryable` flag:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from vpndetection import VPNDetectionError
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
client.lookup("1.1.1.1")
|
|
182
|
+
except VPNDetectionError as err:
|
|
183
|
+
print(err.kind, err.retryable)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`kind` is one of `bad_request`, `unauthorized`, `forbidden`, `rate_limited`, `quota_exceeded`, `server_error` or `network`.
|
|
187
|
+
|
|
188
|
+
Note that `rate_limited` and `quota_exceeded` both arrive as HTTP 429 and are not the same thing. A rate limit is when the API faces extreme traffic bursts and so retrying later works; but a spent quota needs your allowance raised or the window to roll over. The library retries rate limits for you, but not if your quota is exceeded.
|
|
189
|
+
|
|
190
|
+
### Database downloads
|
|
191
|
+
|
|
192
|
+
If your key carries the `db.download` scope, the licensed datasets are available through `client.database`. `list` answers dataset families, and the ids the other calls take come from each family's `versions`. There are three ways to get a database: the time-limited link, the bytes, or straight to a file, which streams so nothing bigger than a chunk is ever held in memory:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
datasets = client.database.list()
|
|
196
|
+
|
|
197
|
+
url = client.database.download_url("vpn_ip_extended_v1", "mmdb")
|
|
198
|
+
raw = client.database.download_bytes("cdn_ip_v1", "csvgz")
|
|
199
|
+
written = client.database.download("vpn_ip_extended_v1", "mmdb", "./vpn_ip_extended_v1.mmdb")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`download_bytes` holds the whole file in memory, and the catalog runs from `cdn_ip_v1` at 10 KB to `resproxy_ip_90d_v1` at 1.79 GB, so use `download` for anything you have not measured.
|
|
203
|
+
|
|
204
|
+
### Fields your plan does not include
|
|
205
|
+
|
|
206
|
+
Only `ip` and `is_vpn` come back on every plan. The rest are `None` when your plan does not include them, which means "not in your plan" rather than "checked, and no".
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
result.flagged("is_hosting") # False rather than None on a plan without it
|
|
210
|
+
result.is_hosting is None # True when hosting is not in your plan
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Other Libraries
|
|
214
|
+
|
|
215
|
+
There are official VPNDetection 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/vpndetection-io for more.
|
|
216
|
+
|
|
217
|
+
## About VPNDetection
|
|
218
|
+
|
|
219
|
+
VPN Detection API: Accurate anonymity detection identifying VPNs, residential proxies, hosting servers, Tor nodes, CDNs, relays and more.
|
|
220
|
+
|
|
221
|
+
[<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="96"/>](https://vpndetection.io/)
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="24"/>](https://vpndetection.io/) VPNDetection Python Client Library
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/vpndetection/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
The official Python client library for the [VPNDetection](https://vpndetection.io) API.
|
|
7
|
+
|
|
8
|
+
The library helps you query VPNDetection's APIs for anonymity detection including VPNs, residential proxies, Tor nodes, hosting servers, CDNs, relays and more.
|
|
9
|
+
|
|
10
|
+
## Getting Started
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install vpndetection
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Python 3.11 or newer. Type hints are included, and the package ships `py.typed`.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
**No API key needed to start.** The free tier answers `ip` and `is_vpn`, and allows 1000 requests per day per source address.
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from vpndetection import VPNDetection
|
|
24
|
+
|
|
25
|
+
client = VPNDetection()
|
|
26
|
+
|
|
27
|
+
result = client.lookup("45.83.91.1")
|
|
28
|
+
print(result.is_vpn) # True
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The client holds an HTTP connection pool, so use it as a context manager, or call `client.close()` when you are done with it:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
with VPNDetection() as client:
|
|
35
|
+
print(client.lookup("45.83.91.1").is_vpn)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### With an API key
|
|
39
|
+
|
|
40
|
+
An API key raises your quota, and raises your features on a paid plan. Create one in the [console](https://app.vpndetection.io), then pass it in:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
client = VPNDetection(os.environ["VPNDETECTION_API_KEY"])
|
|
46
|
+
|
|
47
|
+
result = client.lookup("45.83.91.1")
|
|
48
|
+
print(result.is_vpn) # True
|
|
49
|
+
print(result.vpn.provider) # 'mullvad'
|
|
50
|
+
print(result.is_hosting) # True
|
|
51
|
+
print(result.hosting.provider)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Async
|
|
55
|
+
|
|
56
|
+
Everything above works the same way under asyncio, with `AsyncVPNDetection`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
from vpndetection import AsyncVPNDetection
|
|
61
|
+
|
|
62
|
+
async def main():
|
|
63
|
+
async with AsyncVPNDetection() as client:
|
|
64
|
+
result = await client.lookup("45.83.91.1")
|
|
65
|
+
print(result.is_vpn) # True
|
|
66
|
+
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Batch lookup
|
|
71
|
+
|
|
72
|
+
You can do batch lookups with a list, which parallelizes requests for you efficiently:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
results = client.lookup_batch(["45.83.91.1", "8.8.8.8", "1.1.1.1"])
|
|
76
|
+
|
|
77
|
+
for ip, result in results.items():
|
|
78
|
+
if isinstance(result, Exception):
|
|
79
|
+
print(f"{ip}: {result}")
|
|
80
|
+
continue
|
|
81
|
+
print(f"{ip}: {result.is_vpn}")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Results are keyed by address, so duplicates in your list collapse into a single request and one address failing never loses the rest.
|
|
85
|
+
|
|
86
|
+
Concurrency and other variables are configurable per-call:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
results = client.lookup_batch(many_ips, concurrency=32, retries=4)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Caching
|
|
93
|
+
|
|
94
|
+
Answers are cached by default, so repeat lookups of the same address are free:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
client = VPNDetection()
|
|
98
|
+
|
|
99
|
+
result = client.lookup("45.83.91.1")
|
|
100
|
+
print(result.is_vpn) # True, API request
|
|
101
|
+
|
|
102
|
+
result2 = client.lookup("45.83.91.1")
|
|
103
|
+
print(result2.is_vpn) # True, no API request, result was cached
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
You can change the default cache variables (max size, TTL in seconds, etc) on initialization, or even disable it:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
client = VPNDetection(cache_max_size=50_000, cache_ttl=6 * 60 * 60)
|
|
110
|
+
client_no_cache = VPNDetection(cache=False)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Private and reserved addresses
|
|
114
|
+
|
|
115
|
+
Private, loopback, link-local, documentation and multicast addresses (and their IPv6 equivalents, including the 6to4 and Teredo ranges) can never be VPN or proxy infrastructure. The library answers them locally, so they cost no request and no quota:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
result = client.lookup("192.168.1.1")
|
|
119
|
+
result.is_bogon # True, this answer was computed rather than served
|
|
120
|
+
result.is_vpn # False
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The check is available on the client, which is handy when your inputs are addresses anyway:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
client.is_bogon("10.0.0.1") # True
|
|
127
|
+
client.is_bogon("8.8.8.8") # False
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
It is also importable on its own, if you want it without a client:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from vpndetection import is_bogon
|
|
134
|
+
|
|
135
|
+
is_bogon("10.0.0.1") # True
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Errors
|
|
139
|
+
|
|
140
|
+
Failures raise a `VPNDetectionError` carrying a `kind` and a `retryable` flag:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from vpndetection import VPNDetectionError
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
client.lookup("1.1.1.1")
|
|
147
|
+
except VPNDetectionError as err:
|
|
148
|
+
print(err.kind, err.retryable)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`kind` is one of `bad_request`, `unauthorized`, `forbidden`, `rate_limited`, `quota_exceeded`, `server_error` or `network`.
|
|
152
|
+
|
|
153
|
+
Note that `rate_limited` and `quota_exceeded` both arrive as HTTP 429 and are not the same thing. A rate limit is when the API faces extreme traffic bursts and so retrying later works; but a spent quota needs your allowance raised or the window to roll over. The library retries rate limits for you, but not if your quota is exceeded.
|
|
154
|
+
|
|
155
|
+
### Database downloads
|
|
156
|
+
|
|
157
|
+
If your key carries the `db.download` scope, the licensed datasets are available through `client.database`. `list` answers dataset families, and the ids the other calls take come from each family's `versions`. There are three ways to get a database: the time-limited link, the bytes, or straight to a file, which streams so nothing bigger than a chunk is ever held in memory:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
datasets = client.database.list()
|
|
161
|
+
|
|
162
|
+
url = client.database.download_url("vpn_ip_extended_v1", "mmdb")
|
|
163
|
+
raw = client.database.download_bytes("cdn_ip_v1", "csvgz")
|
|
164
|
+
written = client.database.download("vpn_ip_extended_v1", "mmdb", "./vpn_ip_extended_v1.mmdb")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`download_bytes` holds the whole file in memory, and the catalog runs from `cdn_ip_v1` at 10 KB to `resproxy_ip_90d_v1` at 1.79 GB, so use `download` for anything you have not measured.
|
|
168
|
+
|
|
169
|
+
### Fields your plan does not include
|
|
170
|
+
|
|
171
|
+
Only `ip` and `is_vpn` come back on every plan. The rest are `None` when your plan does not include them, which means "not in your plan" rather than "checked, and no".
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
result.flagged("is_hosting") # False rather than None on a plan without it
|
|
175
|
+
result.is_hosting is None # True when hosting is not in your plan
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Other Libraries
|
|
179
|
+
|
|
180
|
+
There are official VPNDetection 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/vpndetection-io for more.
|
|
181
|
+
|
|
182
|
+
## About VPNDetection
|
|
183
|
+
|
|
184
|
+
VPN Detection API: Accurate anonymity detection identifying VPNs, residential proxies, hosting servers, Tor nodes, CDNs, relays and more.
|
|
185
|
+
|
|
186
|
+
[<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="96"/>](https://vpndetection.io/)
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Runs the integration suite against the package as PUBLISHED on PyPI, which is the one
|
|
4
|
+
thing the unit suite cannot check: that suite tests this working tree, so it stays green
|
|
5
|
+
through a tag that was never pushed, an sdist that ships no `src`, or a wheel a consumer
|
|
6
|
+
cannot import.
|
|
7
|
+
|
|
8
|
+
python3 scripts/run.py # from integration/, with any python3 on PATH
|
|
9
|
+
./scripts/run.sh # the same thing in docker
|
|
10
|
+
|
|
11
|
+
Two conditions make the run meaningless rather than failing, and each one skips with a
|
|
12
|
+
reason instead:
|
|
13
|
+
|
|
14
|
+
1. Nothing on PyPI satisfies the constraint in requirements.txt. Before the first
|
|
15
|
+
release there is no artifact to test.
|
|
16
|
+
2. A tier's staging key is missing. The unauthenticated tests still run, and each tier
|
|
17
|
+
without a key skips from inside the suite, so the skip and its reason land in the
|
|
18
|
+
pytest output rather than in this script's preamble.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
import tempfile
|
|
29
|
+
import venv
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
INTEGRATION = Path(__file__).resolve().parent.parent
|
|
33
|
+
PACKAGE = "vpndetection"
|
|
34
|
+
|
|
35
|
+
# Imported before anything is installed, so it must not touch the package under test.
|
|
36
|
+
sys.path.insert(0, str(INTEGRATION))
|
|
37
|
+
import tiers
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main() -> int:
|
|
41
|
+
constraint = requirement()
|
|
42
|
+
python = build_venv()
|
|
43
|
+
|
|
44
|
+
resolved = resolve(python, constraint)
|
|
45
|
+
if resolved is None:
|
|
46
|
+
skip(f"nothing on PyPI satisfies {constraint}, so there is no published artifact to test")
|
|
47
|
+
return 0
|
|
48
|
+
version, url = resolved
|
|
49
|
+
print(f"==> {constraint} matches published {version} at {url}")
|
|
50
|
+
|
|
51
|
+
with_key = [rung.tier for rung in tiers.observable()]
|
|
52
|
+
absent = [rung.tier for rung in tiers.RUNGS if rung.skip_reason() is not None]
|
|
53
|
+
print(f"==> tiers with a key: {', '.join(with_key)}")
|
|
54
|
+
if absent:
|
|
55
|
+
notice(f"no staging key for {', '.join(absent)}: those tiers are skipped")
|
|
56
|
+
|
|
57
|
+
run([str(python), "-m", "pip", "install", "--quiet", "-r", "requirements.txt"])
|
|
58
|
+
# The gate that proves the suite is testing the release rather than the tree beside it
|
|
59
|
+
# lives in conftest.py, where the import has actually happened.
|
|
60
|
+
return subprocess.run([str(python), "-m", "pytest"], cwd=INTEGRATION, check=False).returncode
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def requirement() -> str:
|
|
64
|
+
"""The package constraint, read from requirements.txt so there is one copy of it."""
|
|
65
|
+
for line in (INTEGRATION / "requirements.txt").read_text().splitlines():
|
|
66
|
+
line = line.split("#")[0].strip()
|
|
67
|
+
if line.startswith(PACKAGE):
|
|
68
|
+
return line
|
|
69
|
+
raise SystemExit(f"requirements.txt names no {PACKAGE} constraint")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def build_venv() -> Path:
|
|
73
|
+
"""A virtualenv of its own, rebuilt every run.
|
|
74
|
+
|
|
75
|
+
Kept rather than reused so a daily run always resolves the constraint afresh: a venv
|
|
76
|
+
carrying yesterday's install would go on testing whatever the first run happened to
|
|
77
|
+
pick and stop noticing new releases.
|
|
78
|
+
"""
|
|
79
|
+
root = INTEGRATION / ".venv"
|
|
80
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
81
|
+
venv.EnvBuilder(with_pip=True, clear=True).create(root)
|
|
82
|
+
python = root / "bin" / "python"
|
|
83
|
+
run([str(python), "-m", "pip", "install", "--quiet", "--upgrade", "pip"])
|
|
84
|
+
return python
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def resolve(python: Path, constraint: str) -> tuple[str, str] | None:
|
|
88
|
+
"""The version pip WOULD install, and where from, or None when nothing satisfies it.
|
|
89
|
+
|
|
90
|
+
This is pip's own resolver rather than a query against the JSON API, so the answer is
|
|
91
|
+
exactly what an install would see, yanked releases and requires-python included. A
|
|
92
|
+
package that does not exist and a constraint nothing satisfies both report the same
|
|
93
|
+
thing, and both mean the same thing here.
|
|
94
|
+
"""
|
|
95
|
+
with tempfile.TemporaryDirectory() as scratch:
|
|
96
|
+
report = Path(scratch) / "report.json"
|
|
97
|
+
result = subprocess.run(
|
|
98
|
+
[
|
|
99
|
+
str(python),
|
|
100
|
+
"-m",
|
|
101
|
+
"pip",
|
|
102
|
+
"install",
|
|
103
|
+
"--dry-run",
|
|
104
|
+
"--no-deps",
|
|
105
|
+
"--quiet",
|
|
106
|
+
"--report",
|
|
107
|
+
str(report),
|
|
108
|
+
constraint,
|
|
109
|
+
],
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
check=False,
|
|
113
|
+
)
|
|
114
|
+
if result.returncode != 0:
|
|
115
|
+
if "no matching distribution" in result.stderr.lower():
|
|
116
|
+
return None
|
|
117
|
+
raise SystemExit(f"pip could not resolve {constraint}:\n{result.stderr}")
|
|
118
|
+
install = json.loads(report.read_text())["install"]
|
|
119
|
+
|
|
120
|
+
version = install[0]["metadata"]["version"]
|
|
121
|
+
url = install[0]["download_info"]["url"]
|
|
122
|
+
# A `file://` here is a local build being passed off as a release, which every test
|
|
123
|
+
# downstream would then pass against.
|
|
124
|
+
if not url.startswith("https://"):
|
|
125
|
+
raise SystemExit(f"{PACKAGE} resolved to {url}, which is not a registry")
|
|
126
|
+
return version, url
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def run(command: list[str]) -> None:
|
|
130
|
+
print(f"==> {' '.join(Path(part).name if '/' in part else part for part in command)}")
|
|
131
|
+
subprocess.run(command, cwd=INTEGRATION, check=True)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def skip(reason: str) -> None:
|
|
135
|
+
print(f"==> SKIPPED: {reason}")
|
|
136
|
+
notice(f"Integration suite skipped: {reason}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def notice(message: str) -> None:
|
|
140
|
+
"""Surfaced on the workflow run itself, so a skip is visible without opening the log
|
|
141
|
+
and reading to the end of it."""
|
|
142
|
+
if os.environ.get("GITHUB_ACTIONS") == "true":
|
|
143
|
+
print(f"::notice title=Integration::{message}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
sys.exit(main())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# The integration suite in docker, for a box whose python has no way to build a
|
|
4
|
+
# virtualenv or reach PyPI. CI runs `python3 scripts/run.py` directly; this is
|
|
5
|
+
# the same entry point with an interpreter around it.
|
|
6
|
+
#
|
|
7
|
+
# ./scripts/run.sh
|
|
8
|
+
#
|
|
9
|
+
# The four tier keys are read from the environment and passed through by NAME, so
|
|
10
|
+
# no key ever reaches a command line. Only the integration directory is mounted:
|
|
11
|
+
# the suite must see the published package rather than the source beside it.
|
|
12
|
+
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
cd "$(dirname "$0")/.."
|
|
16
|
+
|
|
17
|
+
PYTHON_IMAGE="${PYTHON_IMAGE:-python:3.13-slim}"
|
|
18
|
+
|
|
19
|
+
docker run --rm \
|
|
20
|
+
-v "$PWD:/app" -w /app \
|
|
21
|
+
-e PIP_ROOT_USER_ACTION=ignore \
|
|
22
|
+
-e VPNDETECTION_STAGING_KEY_FREE \
|
|
23
|
+
-e VPNDETECTION_STAGING_KEY_STARTER \
|
|
24
|
+
-e VPNDETECTION_STAGING_KEY_SCALE \
|
|
25
|
+
-e VPNDETECTION_STAGING_KEY_MAX \
|
|
26
|
+
"$PYTHON_IMAGE" python3 scripts/run.py "$@"
|