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.
Files changed (69) hide show
  1. vpndetection-1.0.0/.gitignore +17 -0
  2. vpndetection-1.0.0/LICENSE +21 -0
  3. vpndetection-1.0.0/PKG-INFO +225 -0
  4. vpndetection-1.0.0/README.md +190 -0
  5. vpndetection-1.0.0/integration/scripts/run.py +147 -0
  6. vpndetection-1.0.0/integration/scripts/run.sh +26 -0
  7. vpndetection-1.0.0/pyproject.toml +95 -0
  8. vpndetection-1.0.0/scripts/download-spec.sh +18 -0
  9. vpndetection-1.0.0/scripts/generate.sh +47 -0
  10. vpndetection-1.0.0/scripts/publish.sh +47 -0
  11. vpndetection-1.0.0/spec/openapi.yaml +880 -0
  12. vpndetection-1.0.0/src/vpndetection/__init__.py +49 -0
  13. vpndetection-1.0.0/src/vpndetection/_bogons.py +63 -0
  14. vpndetection-1.0.0/src/vpndetection/_core.py +292 -0
  15. vpndetection-1.0.0/src/vpndetection/_generated/__init__.py +8 -0
  16. vpndetection-1.0.0/src/vpndetection/_generated/api/__init__.py +1 -0
  17. vpndetection-1.0.0/src/vpndetection/_generated/api/database/__init__.py +1 -0
  18. vpndetection-1.0.0/src/vpndetection/_generated/api/database/database_checksum.py +203 -0
  19. vpndetection-1.0.0/src/vpndetection/_generated/api/database/database_metadata.py +205 -0
  20. vpndetection-1.0.0/src/vpndetection/_generated/api/database/download_database.py +201 -0
  21. vpndetection-1.0.0/src/vpndetection/_generated/api/database/list_databases.py +134 -0
  22. vpndetection-1.0.0/src/vpndetection/_generated/api/database/list_downloads.py +166 -0
  23. vpndetection-1.0.0/src/vpndetection/_generated/api/lookup/__init__.py +1 -0
  24. vpndetection-1.0.0/src/vpndetection/_generated/api/lookup/lookup_ip.py +197 -0
  25. vpndetection-1.0.0/src/vpndetection/_generated/client.py +272 -0
  26. vpndetection-1.0.0/src/vpndetection/_generated/errors.py +16 -0
  27. vpndetection-1.0.0/src/vpndetection/_generated/models/__init__.py +63 -0
  28. vpndetection-1.0.0/src/vpndetection/_generated/models/class_detail.py +91 -0
  29. vpndetection-1.0.0/src/vpndetection/_generated/models/database_checksum_format.py +9 -0
  30. vpndetection-1.0.0/src/vpndetection/_generated/models/database_checksum_response_200.py +87 -0
  31. vpndetection-1.0.0/src/vpndetection/_generated/models/database_checksum_response_200_checksums.py +88 -0
  32. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_format_size.py +77 -0
  33. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_format_size_format.py +9 -0
  34. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata.py +167 -0
  35. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_column.py +80 -0
  36. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_sample.py +78 -0
  37. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_sample_additional_property_item.py +45 -0
  38. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_sample_size.py +47 -0
  39. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_schema.py +72 -0
  40. vpndetection-1.0.0/src/vpndetection/_generated/models/dataset_metadata_size.py +47 -0
  41. vpndetection-1.0.0/src/vpndetection/_generated/models/download.py +170 -0
  42. vpndetection-1.0.0/src/vpndetection/_generated/models/download_database_format.py +9 -0
  43. vpndetection-1.0.0/src/vpndetection/_generated/models/download_outcome.py +13 -0
  44. vpndetection-1.0.0/src/vpndetection/_generated/models/error.py +61 -0
  45. vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_dataset.py +194 -0
  46. vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_dataset_redistribution.py +10 -0
  47. vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_dataset_standing.py +10 -0
  48. vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_version.py +125 -0
  49. vpndetection-1.0.0/src/vpndetection/_generated/models/licensed_version_sample_formats_item.py +9 -0
  50. vpndetection-1.0.0/src/vpndetection/_generated/models/list_databases_response_200.py +75 -0
  51. vpndetection-1.0.0/src/vpndetection/_generated/models/list_downloads_response_200.py +75 -0
  52. vpndetection-1.0.0/src/vpndetection/_generated/models/lookup_error.py +62 -0
  53. vpndetection-1.0.0/src/vpndetection/_generated/models/lookup_response.py +295 -0
  54. vpndetection-1.0.0/src/vpndetection/_generated/models/proxy_detail.py +129 -0
  55. vpndetection-1.0.0/src/vpndetection/_generated/models/vpn_detail.py +101 -0
  56. vpndetection-1.0.0/src/vpndetection/_generated/types.py +54 -0
  57. vpndetection-1.0.0/src/vpndetection/aio.py +335 -0
  58. vpndetection-1.0.0/src/vpndetection/bogon.py +85 -0
  59. vpndetection-1.0.0/src/vpndetection/client.py +332 -0
  60. vpndetection-1.0.0/src/vpndetection/errors.py +119 -0
  61. vpndetection-1.0.0/src/vpndetection/models.py +190 -0
  62. vpndetection-1.0.0/src/vpndetection/py.typed +0 -0
  63. vpndetection-1.0.0/testdata/bogons.json +60 -0
  64. vpndetection-1.0.0/testdata/testdata.json +744 -0
  65. vpndetection-1.0.0/tests/conftest.py +16 -0
  66. vpndetection-1.0.0/tests/helpers.py +183 -0
  67. vpndetection-1.0.0/tests/test_client.py +141 -0
  68. vpndetection-1.0.0/tests/test_conformance.py +162 -0
  69. 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
+ [![PyPI](https://img.shields.io/pypi/v/vpndetection.svg)](https://pypi.org/project/vpndetection/)
39
+ [![license](https://img.shields.io/pypi/l/vpndetection.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/vpndetection.svg)](https://pypi.org/project/vpndetection/)
4
+ [![license](https://img.shields.io/pypi/l/vpndetection.svg)](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 "$@"