ipwhois-python 1.0.0__tar.gz → 1.0.1__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.
- ipwhois_python-1.0.1/CHANGELOG.md +41 -0
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/PKG-INFO +32 -32
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/README.md +31 -31
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/examples/basic.py +5 -5
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/examples/bulk.py +3 -3
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/examples/defaults.py +6 -6
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/pyproject.toml +1 -1
- ipwhois_python-1.0.1/src/ipwhois/__init__.py +11 -0
- ipwhois_python-1.0.0/src/ipwhois/client.py → ipwhois_python-1.0.1/src/ipwhois/ipwhois.py +18 -16
- ipwhois_python-1.0.0/tests/test_client.py → ipwhois_python-1.0.1/tests/test_ipwhois.py +80 -80
- ipwhois_python-1.0.0/CHANGELOG.md +0 -12
- ipwhois_python-1.0.0/src/ipwhois/__init__.py +0 -11
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/.gitignore +0 -0
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/LICENSE +0 -0
- {ipwhois_python-1.0.0 → ipwhois_python-1.0.1}/src/ipwhois/py.typed +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `ipwhois-python` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.1] - 2026-05-09
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Renamed the main class `Client` to `IPWhois`** for consistency with the
|
|
13
|
+
package and brand. The recommended import is now
|
|
14
|
+
`from ipwhois import IPWhois`. The source module moved from
|
|
15
|
+
`src/ipwhois/client.py` to `src/ipwhois/ipwhois.py`, and the test module
|
|
16
|
+
from `tests/test_client.py` to `tests/test_ipwhois.py`. Public behaviour,
|
|
17
|
+
method signatures, constructor arguments, and return shapes are all
|
|
18
|
+
unchanged.
|
|
19
|
+
|
|
20
|
+
### Migration
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
# Before (1.0.0):
|
|
24
|
+
from ipwhois import Client
|
|
25
|
+
client = Client("YOUR_API_KEY")
|
|
26
|
+
info = client.lookup("8.8.8.8")
|
|
27
|
+
|
|
28
|
+
# After (1.0.1+):
|
|
29
|
+
from ipwhois import IPWhois
|
|
30
|
+
ipwhois = IPWhois("YOUR_API_KEY")
|
|
31
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The variable name (`client`, `ipwhois`, anything else) is up to you; only
|
|
35
|
+
the class identifier changed.
|
|
36
|
+
|
|
37
|
+
## [1.0.0] - 2026-05-08
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- Initial release.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipwhois-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: Official Python client for the ipwhois.io IP Geolocation API. Simple, dependency-free, supports single and bulk IP lookups.
|
|
5
5
|
Project-URL: Homepage, https://ipwhois.io
|
|
6
6
|
Project-URL: Documentation, https://ipwhois.io/documentation
|
|
@@ -53,9 +53,9 @@ Description-Content-Type: text/markdown
|
|
|
53
53
|
|
|
54
54
|
# ipwhois-python
|
|
55
55
|
|
|
56
|
-
[](https://pypi.org/project/ipwhois-python/)
|
|
57
|
-
[](https://pypi.org/project/ipwhois-python/)
|
|
58
|
-
[](LICENSE)
|
|
56
|
+
[](https://pypi.org/project/ipwhois-python/)
|
|
57
|
+
[](https://pypi.org/project/ipwhois-python/)
|
|
58
|
+
[](LICENSE)
|
|
59
59
|
|
|
60
60
|
Official, dependency-free Python client for the [ipwhois.io](https://ipwhois.io) IP Geolocation API.
|
|
61
61
|
|
|
@@ -75,7 +75,7 @@ pip install ipwhois-python
|
|
|
75
75
|
|
|
76
76
|
## Free vs Paid plan
|
|
77
77
|
|
|
78
|
-
The same `
|
|
78
|
+
The same `IPWhois` class is used for both plans. The only difference is whether
|
|
79
79
|
you pass an API key:
|
|
80
80
|
|
|
81
81
|
- **Free plan** — create the client **without arguments**. No API key, no
|
|
@@ -85,10 +85,10 @@ you pass an API key:
|
|
|
85
85
|
threat-detection data.
|
|
86
86
|
|
|
87
87
|
```python
|
|
88
|
-
from ipwhois import
|
|
88
|
+
from ipwhois import IPWhois
|
|
89
89
|
|
|
90
|
-
free =
|
|
91
|
-
paid =
|
|
90
|
+
free = IPWhois() # Free plan — no API key
|
|
91
|
+
paid = IPWhois("YOUR_API_KEY") # Paid plan — with API key
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
Everything else (`lookup()`, options, error handling) is identical.
|
|
@@ -96,11 +96,11 @@ Everything else (`lookup()`, options, error handling) is identical.
|
|
|
96
96
|
## Quick start — Free plan (no API key)
|
|
97
97
|
|
|
98
98
|
```python
|
|
99
|
-
from ipwhois import
|
|
99
|
+
from ipwhois import IPWhois
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
ipwhois = IPWhois() # no API key
|
|
102
102
|
|
|
103
|
-
info =
|
|
103
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
104
104
|
|
|
105
105
|
print(info["country"], info["flag"]["emoji"])
|
|
106
106
|
# → United States 🇺🇸
|
|
@@ -114,11 +114,11 @@ print(f"{info['city']}, {info['region']}")
|
|
|
114
114
|
Get an API key at <https://ipwhois.io> and pass it to the constructor:
|
|
115
115
|
|
|
116
116
|
```python
|
|
117
|
-
from ipwhois import
|
|
117
|
+
from ipwhois import IPWhois
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
ipwhois = IPWhois("YOUR_API_KEY") # with API key
|
|
120
120
|
|
|
121
|
-
info =
|
|
121
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
122
122
|
|
|
123
123
|
print(info["country"], info["flag"]["emoji"])
|
|
124
124
|
# → United States 🇺🇸
|
|
@@ -127,7 +127,7 @@ print(f"{info['city']}, {info['region']}")
|
|
|
127
127
|
# → Mountain View, California
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
> ℹ️ Pass nothing to look up your own public IP: `
|
|
130
|
+
> ℹ️ Pass nothing to look up your own public IP: `ipwhois.lookup()` — works
|
|
131
131
|
> on both plans.
|
|
132
132
|
|
|
133
133
|
## Lookup options
|
|
@@ -149,28 +149,28 @@ If you make many calls with the same options, set them once and forget:
|
|
|
149
149
|
|
|
150
150
|
```python
|
|
151
151
|
# Free plan
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
ipwhois = (
|
|
153
|
+
IPWhois()
|
|
154
154
|
.set_language("en")
|
|
155
155
|
.set_fields(["country", "city", "flag.emoji"])
|
|
156
156
|
.set_timeout(8)
|
|
157
157
|
)
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
ipwhois.lookup("8.8.8.8") # uses all of the above
|
|
160
|
+
ipwhois.lookup("1.1.1.1", lang="de") # per-call options override defaults
|
|
161
161
|
```
|
|
162
162
|
|
|
163
163
|
```python
|
|
164
164
|
# Paid plan
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
ipwhois = (
|
|
166
|
+
IPWhois("YOUR_API_KEY")
|
|
167
167
|
.set_language("en")
|
|
168
168
|
.set_fields(["country", "city", "flag.emoji"])
|
|
169
169
|
.set_timeout(8)
|
|
170
170
|
)
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
ipwhois.lookup("8.8.8.8") # uses all of the above
|
|
173
|
+
ipwhois.lookup("1.1.1.1", lang="de") # per-call options override defaults
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
> ℹ️ Paid plans additionally support `set_security(True)` (Business+) and
|
|
@@ -183,17 +183,17 @@ example, in environments without an up-to-date CA bundle), pass `ssl=False`
|
|
|
183
183
|
to the constructor:
|
|
184
184
|
|
|
185
185
|
```python
|
|
186
|
-
from ipwhois import
|
|
186
|
+
from ipwhois import IPWhois
|
|
187
187
|
|
|
188
188
|
# Free plan
|
|
189
|
-
|
|
189
|
+
ipwhois = IPWhois(ssl=False)
|
|
190
190
|
```
|
|
191
191
|
|
|
192
192
|
```python
|
|
193
|
-
from ipwhois import
|
|
193
|
+
from ipwhois import IPWhois
|
|
194
194
|
|
|
195
195
|
# Paid plan
|
|
196
|
-
|
|
196
|
+
ipwhois = IPWhois("YOUR_API_KEY", ssl=False)
|
|
197
197
|
```
|
|
198
198
|
|
|
199
199
|
> ℹ️ HTTPS is strongly recommended for production traffic — your API key is
|
|
@@ -206,11 +206,11 @@ address counts as one credit. Available on the **Business** and **Unlimited**
|
|
|
206
206
|
plans.
|
|
207
207
|
|
|
208
208
|
```python
|
|
209
|
-
from ipwhois import
|
|
209
|
+
from ipwhois import IPWhois
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
ipwhois = IPWhois("YOUR_API_KEY")
|
|
212
212
|
|
|
213
|
-
results =
|
|
213
|
+
results = ipwhois.bulk_lookup([
|
|
214
214
|
"8.8.8.8",
|
|
215
215
|
"1.1.1.1",
|
|
216
216
|
"208.67.222.222",
|
|
@@ -237,7 +237,7 @@ with `success` set to `False` and a `message`. Just check
|
|
|
237
237
|
`info["success"]` after every call:
|
|
238
238
|
|
|
239
239
|
```python
|
|
240
|
-
info =
|
|
240
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
241
241
|
|
|
242
242
|
if not info["success"]:
|
|
243
243
|
print(f"Lookup failed: {info['message']}")
|
|
@@ -264,7 +264,7 @@ include extra fields you can branch on:
|
|
|
264
264
|
```python
|
|
265
265
|
import time
|
|
266
266
|
|
|
267
|
-
info =
|
|
267
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
268
268
|
|
|
269
269
|
if not info["success"]:
|
|
270
270
|
if info.get("http_status") == 429:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# ipwhois-python
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/ipwhois-python/)
|
|
4
|
-
[](https://pypi.org/project/ipwhois-python/)
|
|
5
|
-
[](LICENSE)
|
|
3
|
+
[](https://pypi.org/project/ipwhois-python/)
|
|
4
|
+
[](https://pypi.org/project/ipwhois-python/)
|
|
5
|
+
[](LICENSE)
|
|
6
6
|
|
|
7
7
|
Official, dependency-free Python client for the [ipwhois.io](https://ipwhois.io) IP Geolocation API.
|
|
8
8
|
|
|
@@ -22,7 +22,7 @@ pip install ipwhois-python
|
|
|
22
22
|
|
|
23
23
|
## Free vs Paid plan
|
|
24
24
|
|
|
25
|
-
The same `
|
|
25
|
+
The same `IPWhois` class is used for both plans. The only difference is whether
|
|
26
26
|
you pass an API key:
|
|
27
27
|
|
|
28
28
|
- **Free plan** — create the client **without arguments**. No API key, no
|
|
@@ -32,10 +32,10 @@ you pass an API key:
|
|
|
32
32
|
threat-detection data.
|
|
33
33
|
|
|
34
34
|
```python
|
|
35
|
-
from ipwhois import
|
|
35
|
+
from ipwhois import IPWhois
|
|
36
36
|
|
|
37
|
-
free =
|
|
38
|
-
paid =
|
|
37
|
+
free = IPWhois() # Free plan — no API key
|
|
38
|
+
paid = IPWhois("YOUR_API_KEY") # Paid plan — with API key
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Everything else (`lookup()`, options, error handling) is identical.
|
|
@@ -43,11 +43,11 @@ Everything else (`lookup()`, options, error handling) is identical.
|
|
|
43
43
|
## Quick start — Free plan (no API key)
|
|
44
44
|
|
|
45
45
|
```python
|
|
46
|
-
from ipwhois import
|
|
46
|
+
from ipwhois import IPWhois
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
ipwhois = IPWhois() # no API key
|
|
49
49
|
|
|
50
|
-
info =
|
|
50
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
51
51
|
|
|
52
52
|
print(info["country"], info["flag"]["emoji"])
|
|
53
53
|
# → United States 🇺🇸
|
|
@@ -61,11 +61,11 @@ print(f"{info['city']}, {info['region']}")
|
|
|
61
61
|
Get an API key at <https://ipwhois.io> and pass it to the constructor:
|
|
62
62
|
|
|
63
63
|
```python
|
|
64
|
-
from ipwhois import
|
|
64
|
+
from ipwhois import IPWhois
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
ipwhois = IPWhois("YOUR_API_KEY") # with API key
|
|
67
67
|
|
|
68
|
-
info =
|
|
68
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
69
69
|
|
|
70
70
|
print(info["country"], info["flag"]["emoji"])
|
|
71
71
|
# → United States 🇺🇸
|
|
@@ -74,7 +74,7 @@ print(f"{info['city']}, {info['region']}")
|
|
|
74
74
|
# → Mountain View, California
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
> ℹ️ Pass nothing to look up your own public IP: `
|
|
77
|
+
> ℹ️ Pass nothing to look up your own public IP: `ipwhois.lookup()` — works
|
|
78
78
|
> on both plans.
|
|
79
79
|
|
|
80
80
|
## Lookup options
|
|
@@ -96,28 +96,28 @@ If you make many calls with the same options, set them once and forget:
|
|
|
96
96
|
|
|
97
97
|
```python
|
|
98
98
|
# Free plan
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
ipwhois = (
|
|
100
|
+
IPWhois()
|
|
101
101
|
.set_language("en")
|
|
102
102
|
.set_fields(["country", "city", "flag.emoji"])
|
|
103
103
|
.set_timeout(8)
|
|
104
104
|
)
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
ipwhois.lookup("8.8.8.8") # uses all of the above
|
|
107
|
+
ipwhois.lookup("1.1.1.1", lang="de") # per-call options override defaults
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
```python
|
|
111
111
|
# Paid plan
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
ipwhois = (
|
|
113
|
+
IPWhois("YOUR_API_KEY")
|
|
114
114
|
.set_language("en")
|
|
115
115
|
.set_fields(["country", "city", "flag.emoji"])
|
|
116
116
|
.set_timeout(8)
|
|
117
117
|
)
|
|
118
118
|
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
ipwhois.lookup("8.8.8.8") # uses all of the above
|
|
120
|
+
ipwhois.lookup("1.1.1.1", lang="de") # per-call options override defaults
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
> ℹ️ Paid plans additionally support `set_security(True)` (Business+) and
|
|
@@ -130,17 +130,17 @@ example, in environments without an up-to-date CA bundle), pass `ssl=False`
|
|
|
130
130
|
to the constructor:
|
|
131
131
|
|
|
132
132
|
```python
|
|
133
|
-
from ipwhois import
|
|
133
|
+
from ipwhois import IPWhois
|
|
134
134
|
|
|
135
135
|
# Free plan
|
|
136
|
-
|
|
136
|
+
ipwhois = IPWhois(ssl=False)
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
```python
|
|
140
|
-
from ipwhois import
|
|
140
|
+
from ipwhois import IPWhois
|
|
141
141
|
|
|
142
142
|
# Paid plan
|
|
143
|
-
|
|
143
|
+
ipwhois = IPWhois("YOUR_API_KEY", ssl=False)
|
|
144
144
|
```
|
|
145
145
|
|
|
146
146
|
> ℹ️ HTTPS is strongly recommended for production traffic — your API key is
|
|
@@ -153,11 +153,11 @@ address counts as one credit. Available on the **Business** and **Unlimited**
|
|
|
153
153
|
plans.
|
|
154
154
|
|
|
155
155
|
```python
|
|
156
|
-
from ipwhois import
|
|
156
|
+
from ipwhois import IPWhois
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
ipwhois = IPWhois("YOUR_API_KEY")
|
|
159
159
|
|
|
160
|
-
results =
|
|
160
|
+
results = ipwhois.bulk_lookup([
|
|
161
161
|
"8.8.8.8",
|
|
162
162
|
"1.1.1.1",
|
|
163
163
|
"208.67.222.222",
|
|
@@ -184,7 +184,7 @@ with `success` set to `False` and a `message`. Just check
|
|
|
184
184
|
`info["success"]` after every call:
|
|
185
185
|
|
|
186
186
|
```python
|
|
187
|
-
info =
|
|
187
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
188
188
|
|
|
189
189
|
if not info["success"]:
|
|
190
190
|
print(f"Lookup failed: {info['message']}")
|
|
@@ -211,7 +211,7 @@ include extra fields you can branch on:
|
|
|
211
211
|
```python
|
|
212
212
|
import time
|
|
213
213
|
|
|
214
|
-
info =
|
|
214
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
215
215
|
|
|
216
216
|
if not info["success"]:
|
|
217
217
|
if info.get("http_status") == 429:
|
|
@@ -4,15 +4,15 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
-
from ipwhois import
|
|
7
|
+
from ipwhois import IPWhois
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
# -----------------------------------------------------------------------
|
|
11
11
|
# 1) Free plan -- no API key, ~1 request/second per client IP.
|
|
12
12
|
# -----------------------------------------------------------------------
|
|
13
|
-
|
|
13
|
+
ipwhois = IPWhois()
|
|
14
14
|
|
|
15
|
-
info =
|
|
15
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
16
16
|
|
|
17
17
|
# All errors -- invalid IP, network failure, bad options, ... -- come back
|
|
18
18
|
# here with success == False. The library never raises.
|
|
@@ -33,7 +33,7 @@ print(
|
|
|
33
33
|
# -----------------------------------------------------------------------
|
|
34
34
|
# 2) Look up the caller's own IP -- pass nothing (or None).
|
|
35
35
|
# -----------------------------------------------------------------------
|
|
36
|
-
me =
|
|
36
|
+
me = ipwhois.lookup()
|
|
37
37
|
if me["success"]:
|
|
38
38
|
print(f"My IP: {me['ip']} -- {me['country']}")
|
|
39
39
|
|
|
@@ -41,7 +41,7 @@ if me["success"]:
|
|
|
41
41
|
# -----------------------------------------------------------------------
|
|
42
42
|
# 3) Paid plan -- supply the API key.
|
|
43
43
|
# -----------------------------------------------------------------------
|
|
44
|
-
paid =
|
|
44
|
+
paid = IPWhois("YOUR_API_KEY")
|
|
45
45
|
|
|
46
46
|
info = paid.lookup(
|
|
47
47
|
"1.1.1.1",
|
|
@@ -13,10 +13,10 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import sys
|
|
15
15
|
|
|
16
|
-
from ipwhois import
|
|
16
|
+
from ipwhois import IPWhois
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
ipwhois = IPWhois("YOUR_API_KEY")
|
|
20
20
|
|
|
21
21
|
ips = [
|
|
22
22
|
"8.8.8.8",
|
|
@@ -25,7 +25,7 @@ ips = [
|
|
|
25
25
|
"2c0f:fb50:4003::", # IPv6 is fine too -- mix freely
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
-
results =
|
|
28
|
+
results = ipwhois.bulk_lookup(ips, lang="en", security=True)
|
|
29
29
|
|
|
30
30
|
# Whole-batch failure (network down, bad API key, rate limit, ...) -- the
|
|
31
31
|
# response is a single error dict instead of a list of per-IP results.
|
|
@@ -8,11 +8,11 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import sys
|
|
10
10
|
|
|
11
|
-
from ipwhois import
|
|
11
|
+
from ipwhois import IPWhois
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
ipwhois = (
|
|
15
|
+
IPWhois("YOUR_API_KEY")
|
|
16
16
|
.set_language("en")
|
|
17
17
|
.set_fields(["country", "city", "flag.emoji", "connection.isp"])
|
|
18
18
|
.set_security(True)
|
|
@@ -30,12 +30,12 @@ def show(label: str, info: dict) -> None:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
# Both calls below will use lang=en, the field whitelist, and security=1.
|
|
33
|
-
google =
|
|
34
|
-
cf =
|
|
33
|
+
google = ipwhois.lookup("8.8.8.8")
|
|
34
|
+
cf = ipwhois.lookup("1.1.1.1")
|
|
35
35
|
|
|
36
36
|
show("8.8.8.8", google)
|
|
37
37
|
show("1.1.1.1", cf)
|
|
38
38
|
|
|
39
39
|
# One-off override -- this single call uses German instead of English.
|
|
40
|
-
de_only =
|
|
40
|
+
de_only = ipwhois.lookup("8.8.4.4", lang="de")
|
|
41
41
|
show("8.8.4.4 (de)", de_only)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ipwhois-python"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.1"
|
|
8
8
|
description = "Official Python client for the ipwhois.io IP Geolocation API. Simple, dependency-free, supports single and bulk IP lookups."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Official Python client for the ipwhois.io IP Geolocation API.
|
|
2
|
+
|
|
3
|
+
See :class:`ipwhois.IPWhois` for usage. The library never raises -- every
|
|
4
|
+
failure comes back inside the response dict with ``success`` set to ``False``
|
|
5
|
+
and a ``message``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .ipwhois import IPWhois
|
|
9
|
+
|
|
10
|
+
__all__ = ["IPWhois", "__version__"]
|
|
11
|
+
__version__ = IPWhois.VERSION
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Quick start
|
|
4
4
|
-----------
|
|
5
|
+
from ipwhois import IPWhois
|
|
6
|
+
|
|
5
7
|
# Free plan (no API key, ~1 request/second per client IP)
|
|
6
|
-
|
|
7
|
-
info
|
|
8
|
+
ipwhois = IPWhois()
|
|
9
|
+
info = ipwhois.lookup("8.8.8.8")
|
|
8
10
|
|
|
9
11
|
# Paid plan (with API key, higher limits, bulk, security data, ...)
|
|
10
|
-
|
|
11
|
-
info
|
|
12
|
+
ipwhois = IPWhois("YOUR_API_KEY")
|
|
13
|
+
info = ipwhois.lookup("8.8.8.8", lang="en", security=True)
|
|
12
14
|
|
|
13
15
|
# Bulk lookup -- up to 100 IPs in one call (paid only)
|
|
14
|
-
rows =
|
|
16
|
+
rows = ipwhois.bulk_lookup(["8.8.8.8", "1.1.1.1", "208.67.222.222"])
|
|
15
17
|
|
|
16
18
|
# HTTPS is enabled by default. Pass ssl=False to fall back to HTTP.
|
|
17
19
|
|
|
@@ -32,7 +34,7 @@ import urllib.parse
|
|
|
32
34
|
import urllib.request
|
|
33
35
|
from typing import Any, Dict, Iterable, List, Optional, Union
|
|
34
36
|
|
|
35
|
-
__all__ = ["
|
|
37
|
+
__all__ = ["IPWhois"]
|
|
36
38
|
|
|
37
39
|
# Public type alias: a single API response (lookup or whole-batch error).
|
|
38
40
|
Response = Dict[str, Any]
|
|
@@ -40,7 +42,7 @@ Response = Dict[str, Any]
|
|
|
40
42
|
BulkResponse = Union[List[Response], Response]
|
|
41
43
|
|
|
42
44
|
|
|
43
|
-
class
|
|
45
|
+
class IPWhois:
|
|
44
46
|
"""Client for the ipwhois.io IP Geolocation API.
|
|
45
47
|
|
|
46
48
|
The same class is used for both the Free and Paid plans -- the only
|
|
@@ -53,7 +55,7 @@ class Client:
|
|
|
53
55
|
"""
|
|
54
56
|
|
|
55
57
|
#: Library version, used in the default User-Agent header.
|
|
56
|
-
VERSION: str = "1.0.
|
|
58
|
+
VERSION: str = "1.0.1"
|
|
57
59
|
|
|
58
60
|
#: Free-plan endpoint host (used when no API key is provided).
|
|
59
61
|
HOST_FREE: str = "ipwho.is"
|
|
@@ -217,7 +219,7 @@ class Client:
|
|
|
217
219
|
|
|
218
220
|
# -- Fluent setters ------------------------------------------------- #
|
|
219
221
|
|
|
220
|
-
def set_language(self, lang: str) -> "
|
|
222
|
+
def set_language(self, lang: str) -> "IPWhois":
|
|
221
223
|
"""Set the default language used when none is supplied per call.
|
|
222
224
|
|
|
223
225
|
:param lang: One of :attr:`SUPPORTED_LANGUAGES`.
|
|
@@ -227,7 +229,7 @@ class Client:
|
|
|
227
229
|
|
|
228
230
|
def set_fields(
|
|
229
231
|
self, fields: Union[str, Iterable[str], None]
|
|
230
|
-
) -> "
|
|
232
|
+
) -> "IPWhois":
|
|
231
233
|
"""Restrict every response to a fixed set of fields by default.
|
|
232
234
|
|
|
233
235
|
:param fields: An iterable of field names, e.g.
|
|
@@ -251,17 +253,17 @@ class Client:
|
|
|
251
253
|
self._defaults["fields"] = str(fields)
|
|
252
254
|
return self
|
|
253
255
|
|
|
254
|
-
def set_security(self, enabled: bool) -> "
|
|
256
|
+
def set_security(self, enabled: bool) -> "IPWhois":
|
|
255
257
|
"""Enable or disable threat-detection data on every call by default."""
|
|
256
258
|
self._defaults["security"] = bool(enabled)
|
|
257
259
|
return self
|
|
258
260
|
|
|
259
|
-
def set_rate(self, enabled: bool) -> "
|
|
261
|
+
def set_rate(self, enabled: bool) -> "IPWhois":
|
|
260
262
|
"""Enable or disable the ``rate`` block in responses by default."""
|
|
261
263
|
self._defaults["rate"] = bool(enabled)
|
|
262
264
|
return self
|
|
263
265
|
|
|
264
|
-
def set_timeout(self, seconds: Any) -> "
|
|
266
|
+
def set_timeout(self, seconds: Any) -> "IPWhois":
|
|
265
267
|
"""Set the per-request total timeout in seconds (default: 10).
|
|
266
268
|
|
|
267
269
|
Bad values (non-numeric, negative) silently fall back to the default,
|
|
@@ -270,7 +272,7 @@ class Client:
|
|
|
270
272
|
self._timeout = _coerce_positive_int(seconds, self._timeout)
|
|
271
273
|
return self
|
|
272
274
|
|
|
273
|
-
def set_connect_timeout(self, seconds: Any) -> "
|
|
275
|
+
def set_connect_timeout(self, seconds: Any) -> "IPWhois":
|
|
274
276
|
"""Set the connection timeout in seconds (default: 5).
|
|
275
277
|
|
|
276
278
|
Note: Python's :mod:`urllib` exposes a single timeout that covers
|
|
@@ -287,7 +289,7 @@ class Client:
|
|
|
287
289
|
)
|
|
288
290
|
return self
|
|
289
291
|
|
|
290
|
-
def set_user_agent(self, user_agent: str) -> "
|
|
292
|
+
def set_user_agent(self, user_agent: str) -> "IPWhois":
|
|
291
293
|
"""Override the User-Agent header sent with every request."""
|
|
292
294
|
self._user_agent = str(user_agent)
|
|
293
295
|
return self
|
|
@@ -502,7 +504,7 @@ def _coerce_positive_int(value: Any, default: int) -> int:
|
|
|
502
504
|
"""Coerce ``value`` to a positive int, falling back to ``default``.
|
|
503
505
|
|
|
504
506
|
Mirrors PHP's lenient ``(int)`` cast so that a stray ``"foo"`` passed
|
|
505
|
-
via constructor kwargs or :meth:`
|
|
507
|
+
via constructor kwargs or :meth:`IPWhois.set_timeout` doesn't blow up
|
|
506
508
|
the whole client. ``None``, non-numeric strings, ``True``/``False``
|
|
507
509
|
edge cases, negative numbers and zero all map to ``default``.
|
|
508
510
|
"""
|
|
@@ -6,34 +6,34 @@ the suite can be run anywhere without an API key or network access.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from ipwhois import
|
|
9
|
+
from ipwhois import IPWhois
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def _build_url(
|
|
13
|
-
return
|
|
12
|
+
def _build_url(ipwhois: IPWhois, path: str, **options) -> str:
|
|
13
|
+
return ipwhois._build_url(path, options)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def test_free_endpoint_has_no_api_key() -> None:
|
|
17
|
-
|
|
18
|
-
assert _build_url(
|
|
17
|
+
ipwhois = IPWhois()
|
|
18
|
+
assert _build_url(ipwhois, "/8.8.8.8") == "https://ipwho.is/8.8.8.8"
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def test_paid_endpoint_appends_api_key() -> None:
|
|
22
|
-
|
|
23
|
-
url = _build_url(
|
|
22
|
+
ipwhois = IPWhois("TESTKEY")
|
|
23
|
+
url = _build_url(ipwhois, "/8.8.8.8")
|
|
24
24
|
|
|
25
25
|
assert url.startswith("https://ipwhois.pro/8.8.8.8?")
|
|
26
26
|
assert "key=TESTKEY" in url
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def test_https_is_always_used_by_default() -> None:
|
|
30
|
-
assert _build_url(
|
|
31
|
-
assert _build_url(
|
|
30
|
+
assert _build_url(IPWhois(), "/").startswith("https://")
|
|
31
|
+
assert _build_url(IPWhois("K"), "/").startswith("https://")
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def test_ssl_can_be_disabled() -> None:
|
|
35
|
-
free =
|
|
36
|
-
paid =
|
|
35
|
+
free = IPWhois(ssl=False)
|
|
36
|
+
paid = IPWhois("K", ssl=False)
|
|
37
37
|
|
|
38
38
|
assert _build_url(free, "/").startswith("http://ipwho.is")
|
|
39
39
|
assert _build_url(paid, "/").startswith("http://ipwhois.pro")
|
|
@@ -41,13 +41,13 @@ def test_ssl_can_be_disabled() -> None:
|
|
|
41
41
|
|
|
42
42
|
def test_ssl_defaults_to_true_when_not_passed() -> None:
|
|
43
43
|
# Sanity check: omitting the option keeps HTTPS on.
|
|
44
|
-
assert _build_url(
|
|
44
|
+
assert _build_url(IPWhois("K"), "/").startswith("https://")
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def test_fields_are_joined_with_commas() -> None:
|
|
48
|
-
|
|
48
|
+
ipwhois = IPWhois("K")
|
|
49
49
|
url = _build_url(
|
|
50
|
-
|
|
50
|
+
ipwhois, "/8.8.8.8", fields=["country", "city", "flag.emoji"]
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
# urlencode encodes commas as %2C -- both forms are valid HTTP.
|
|
@@ -55,36 +55,36 @@ def test_fields_are_joined_with_commas() -> None:
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def test_fields_accepts_a_string_too() -> None:
|
|
58
|
-
|
|
59
|
-
url = _build_url(
|
|
58
|
+
ipwhois = IPWhois("K")
|
|
59
|
+
url = _build_url(ipwhois, "/8.8.8.8", fields="country,city")
|
|
60
60
|
assert "fields=country%2Ccity" in url
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def test_security_and_rate_are_flags_not_values() -> None:
|
|
64
|
-
|
|
65
|
-
url = _build_url(
|
|
64
|
+
ipwhois = IPWhois("K")
|
|
65
|
+
url = _build_url(ipwhois, "/", security=True, rate=True)
|
|
66
66
|
|
|
67
67
|
assert "security=1" in url
|
|
68
68
|
assert "rate=1" in url
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def test_security_false_is_omitted() -> None:
|
|
72
|
-
|
|
73
|
-
url = _build_url(
|
|
72
|
+
ipwhois = IPWhois("K")
|
|
73
|
+
url = _build_url(ipwhois, "/", security=False)
|
|
74
74
|
|
|
75
75
|
assert "security=" not in url
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def test_per_call_options_override_defaults() -> None:
|
|
79
|
-
|
|
80
|
-
url = _build_url(
|
|
79
|
+
ipwhois = IPWhois("K", lang="ru")
|
|
80
|
+
url = _build_url(ipwhois, "/", lang="en")
|
|
81
81
|
|
|
82
82
|
assert "lang=en" in url
|
|
83
83
|
assert "lang=ru" not in url
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
def test_invalid_language_returns_error_dict() -> None:
|
|
87
|
-
result =
|
|
87
|
+
result = IPWhois().lookup("8.8.8.8", lang="klingon")
|
|
88
88
|
|
|
89
89
|
assert result["success"] is False
|
|
90
90
|
assert result.get("error_type") == "invalid_argument"
|
|
@@ -92,7 +92,7 @@ def test_invalid_language_returns_error_dict() -> None:
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def test_invalid_output_returns_error_dict() -> None:
|
|
95
|
-
result =
|
|
95
|
+
result = IPWhois().lookup("8.8.8.8", output="yaml")
|
|
96
96
|
|
|
97
97
|
assert result["success"] is False
|
|
98
98
|
assert result.get("error_type") == "invalid_argument"
|
|
@@ -100,7 +100,7 @@ def test_invalid_output_returns_error_dict() -> None:
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def test_bulk_lookup_refuses_empty_list() -> None:
|
|
103
|
-
result =
|
|
103
|
+
result = IPWhois("K").bulk_lookup([])
|
|
104
104
|
|
|
105
105
|
assert isinstance(result, dict)
|
|
106
106
|
assert result["success"] is False
|
|
@@ -108,8 +108,8 @@ def test_bulk_lookup_refuses_empty_list() -> None:
|
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
def test_bulk_lookup_refuses_more_than_limit() -> None:
|
|
111
|
-
too_many = ["8.8.8.8"] * (
|
|
112
|
-
result =
|
|
111
|
+
too_many = ["8.8.8.8"] * (IPWhois.BULK_LIMIT + 1)
|
|
112
|
+
result = IPWhois("K").bulk_lookup(too_many)
|
|
113
113
|
|
|
114
114
|
assert isinstance(result, dict)
|
|
115
115
|
assert result["success"] is False
|
|
@@ -117,60 +117,60 @@ def test_bulk_lookup_refuses_more_than_limit() -> None:
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
def test_bulk_url_is_comma_separated() -> None:
|
|
120
|
-
|
|
121
|
-
url = _build_url(
|
|
120
|
+
ipwhois = IPWhois("K")
|
|
121
|
+
url = _build_url(ipwhois, "/bulk/" + ",".join(["8.8.8.8", "1.1.1.1"]))
|
|
122
122
|
|
|
123
123
|
assert "/bulk/8.8.8.8,1.1.1.1" in url
|
|
124
124
|
|
|
125
125
|
|
|
126
126
|
def test_fluent_setters_return_self() -> None:
|
|
127
|
-
|
|
127
|
+
ipwhois = IPWhois()
|
|
128
128
|
|
|
129
|
-
assert
|
|
130
|
-
assert
|
|
131
|
-
assert
|
|
132
|
-
assert
|
|
133
|
-
assert
|
|
134
|
-
assert
|
|
135
|
-
assert
|
|
129
|
+
assert ipwhois.set_language("en") is ipwhois
|
|
130
|
+
assert ipwhois.set_fields(["country"]) is ipwhois
|
|
131
|
+
assert ipwhois.set_security(True) is ipwhois
|
|
132
|
+
assert ipwhois.set_rate(False) is ipwhois
|
|
133
|
+
assert ipwhois.set_timeout(5) is ipwhois
|
|
134
|
+
assert ipwhois.set_connect_timeout(2) is ipwhois
|
|
135
|
+
assert ipwhois.set_user_agent("test/1.0") is ipwhois
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
def test_set_language_affects_subsequent_requests() -> None:
|
|
139
|
-
|
|
140
|
-
url = _build_url(
|
|
139
|
+
ipwhois = IPWhois("K").set_language("de")
|
|
140
|
+
url = _build_url(ipwhois, "/")
|
|
141
141
|
|
|
142
142
|
assert "lang=de" in url
|
|
143
143
|
|
|
144
144
|
|
|
145
145
|
def test_set_fields_affects_subsequent_requests() -> None:
|
|
146
|
-
|
|
147
|
-
url = _build_url(
|
|
146
|
+
ipwhois = IPWhois("K").set_fields(["country", "city"])
|
|
147
|
+
url = _build_url(ipwhois, "/8.8.8.8")
|
|
148
148
|
|
|
149
149
|
assert "fields=country%2Ccity" in url
|
|
150
150
|
|
|
151
151
|
|
|
152
152
|
def test_constructor_options_become_defaults() -> None:
|
|
153
|
-
|
|
154
|
-
url = _build_url(
|
|
153
|
+
ipwhois = IPWhois("K", lang="ru", security=True)
|
|
154
|
+
url = _build_url(ipwhois, "/8.8.8.8")
|
|
155
155
|
|
|
156
156
|
assert "lang=ru" in url
|
|
157
157
|
assert "security=1" in url
|
|
158
158
|
|
|
159
159
|
|
|
160
160
|
def test_user_agent_can_be_set_in_constructor() -> None:
|
|
161
|
-
|
|
162
|
-
assert
|
|
161
|
+
ipwhois = IPWhois(user_agent="my-app/2.0")
|
|
162
|
+
assert ipwhois._user_agent == "my-app/2.0"
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
def test_default_user_agent_includes_version() -> None:
|
|
166
|
-
|
|
167
|
-
assert
|
|
166
|
+
ipwhois = IPWhois()
|
|
167
|
+
assert ipwhois._user_agent == f"ipwhois-python/{IPWhois.VERSION}"
|
|
168
168
|
|
|
169
169
|
|
|
170
170
|
def test_ip_is_url_encoded_for_ipv6() -> None:
|
|
171
171
|
# IPv6 colons must be percent-encoded inside a path segment.
|
|
172
|
-
|
|
173
|
-
url = _build_url(
|
|
172
|
+
ipwhois = IPWhois()
|
|
173
|
+
url = _build_url(ipwhois, "/" + __import__("urllib.parse", fromlist=["quote"]).quote("2c0f:fb50:4003::", safe=""))
|
|
174
174
|
assert "%3A" in url
|
|
175
175
|
|
|
176
176
|
|
|
@@ -182,7 +182,7 @@ def test_ip_is_url_encoded_for_ipv6() -> None:
|
|
|
182
182
|
def test_bulk_lookup_rejects_a_single_string() -> None:
|
|
183
183
|
# Strings are iterable in Python; without the guard, "8.8.8.8" would
|
|
184
184
|
# be looked up character-by-character. Reject explicitly.
|
|
185
|
-
result =
|
|
185
|
+
result = IPWhois("K").bulk_lookup("8.8.8.8") # type: ignore[arg-type]
|
|
186
186
|
|
|
187
187
|
assert isinstance(result, dict)
|
|
188
188
|
assert result["success"] is False
|
|
@@ -191,7 +191,7 @@ def test_bulk_lookup_rejects_a_single_string() -> None:
|
|
|
191
191
|
|
|
192
192
|
|
|
193
193
|
def test_bulk_lookup_rejects_bytes() -> None:
|
|
194
|
-
result =
|
|
194
|
+
result = IPWhois("K").bulk_lookup(b"8.8.8.8") # type: ignore[arg-type]
|
|
195
195
|
|
|
196
196
|
assert isinstance(result, dict)
|
|
197
197
|
assert result["success"] is False
|
|
@@ -199,7 +199,7 @@ def test_bulk_lookup_rejects_bytes() -> None:
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def test_bulk_lookup_handles_none_gracefully() -> None:
|
|
202
|
-
result =
|
|
202
|
+
result = IPWhois("K").bulk_lookup(None) # type: ignore[arg-type]
|
|
203
203
|
|
|
204
204
|
assert isinstance(result, dict)
|
|
205
205
|
assert result["success"] is False
|
|
@@ -207,7 +207,7 @@ def test_bulk_lookup_handles_none_gracefully() -> None:
|
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
def test_bulk_lookup_handles_non_iterable_gracefully() -> None:
|
|
210
|
-
result =
|
|
210
|
+
result = IPWhois("K").bulk_lookup(42) # type: ignore[arg-type]
|
|
211
211
|
|
|
212
212
|
assert isinstance(result, dict)
|
|
213
213
|
assert result["success"] is False
|
|
@@ -215,14 +215,14 @@ def test_bulk_lookup_handles_non_iterable_gracefully() -> None:
|
|
|
215
215
|
|
|
216
216
|
|
|
217
217
|
def test_bulk_lookup_accepts_a_generator() -> None:
|
|
218
|
-
|
|
218
|
+
ipwhois = IPWhois("K")
|
|
219
219
|
# A generator is an iterable that's not a list -- make sure it works.
|
|
220
220
|
gen = (ip for ip in ["8.8.8.8", "1.1.1.1"])
|
|
221
221
|
# Just check it gets through validation -- we don't actually hit the
|
|
222
222
|
# network. Validation passes if we don't get an `error_type` back.
|
|
223
223
|
# We trigger the guard *before* network by passing an empty generator:
|
|
224
224
|
empty = (x for x in [])
|
|
225
|
-
result =
|
|
225
|
+
result = ipwhois.bulk_lookup(empty)
|
|
226
226
|
assert isinstance(result, dict)
|
|
227
227
|
assert result["success"] is False
|
|
228
228
|
assert result.get("error_type") == "invalid_argument"
|
|
@@ -233,8 +233,8 @@ def test_bulk_lookup_accepts_a_generator() -> None:
|
|
|
233
233
|
|
|
234
234
|
def test_set_fields_keeps_string_intact() -> None:
|
|
235
235
|
# Without the str guard, list("country,city") explodes into characters.
|
|
236
|
-
|
|
237
|
-
url = _build_url(
|
|
236
|
+
ipwhois = IPWhois("K").set_fields("country,city")
|
|
237
|
+
url = _build_url(ipwhois, "/8.8.8.8")
|
|
238
238
|
|
|
239
239
|
assert "fields=country%2Ccity" in url
|
|
240
240
|
# Sanity: confirm we haven't accidentally produced the broken form.
|
|
@@ -243,58 +243,58 @@ def test_set_fields_keeps_string_intact() -> None:
|
|
|
243
243
|
|
|
244
244
|
def test_set_fields_tolerates_none() -> None:
|
|
245
245
|
# set_fields(None) must not raise; it clears any previously-set default.
|
|
246
|
-
|
|
247
|
-
url =
|
|
246
|
+
ipwhois = IPWhois().set_fields(["country"]).set_fields(None)
|
|
247
|
+
url = ipwhois._build_url("/8.8.8.8", {})
|
|
248
248
|
assert "fields=" not in url
|
|
249
249
|
|
|
250
250
|
|
|
251
251
|
def test_set_fields_tolerates_non_iterable() -> None:
|
|
252
252
|
# Non-iterable, non-string garbage falls back to str() rather than raising.
|
|
253
|
-
|
|
254
|
-
url = _build_url(
|
|
253
|
+
ipwhois = IPWhois("K").set_fields(42) # type: ignore[arg-type]
|
|
254
|
+
url = _build_url(ipwhois, "/8.8.8.8")
|
|
255
255
|
assert "fields=42" in url
|
|
256
256
|
|
|
257
257
|
|
|
258
258
|
def test_set_timeout_tolerates_garbage_input() -> None:
|
|
259
259
|
# "never raises" extends to setters: bad input falls back to previous
|
|
260
260
|
# value rather than raising ValueError.
|
|
261
|
-
|
|
262
|
-
previous =
|
|
261
|
+
ipwhois = IPWhois()
|
|
262
|
+
previous = ipwhois._timeout
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
assert
|
|
264
|
+
ipwhois.set_timeout("not a number") # type: ignore[arg-type]
|
|
265
|
+
assert ipwhois._timeout == previous
|
|
266
266
|
|
|
267
|
-
|
|
268
|
-
assert
|
|
267
|
+
ipwhois.set_timeout(None) # type: ignore[arg-type]
|
|
268
|
+
assert ipwhois._timeout == previous
|
|
269
269
|
|
|
270
|
-
|
|
271
|
-
assert
|
|
270
|
+
ipwhois.set_timeout(-5)
|
|
271
|
+
assert ipwhois._timeout == previous
|
|
272
272
|
|
|
273
273
|
# Numeric strings are accepted (matches PHP's `(int)` behaviour).
|
|
274
|
-
|
|
275
|
-
assert
|
|
274
|
+
ipwhois.set_timeout("15") # type: ignore[arg-type]
|
|
275
|
+
assert ipwhois._timeout == 15
|
|
276
276
|
|
|
277
277
|
|
|
278
278
|
def test_set_connect_timeout_tolerates_garbage_input() -> None:
|
|
279
|
-
|
|
280
|
-
previous =
|
|
279
|
+
ipwhois = IPWhois()
|
|
280
|
+
previous = ipwhois._connect_timeout
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
assert
|
|
282
|
+
ipwhois.set_connect_timeout("oops") # type: ignore[arg-type]
|
|
283
|
+
assert ipwhois._connect_timeout == previous
|
|
284
284
|
|
|
285
285
|
|
|
286
286
|
def test_constructor_tolerates_bad_timeout() -> None:
|
|
287
287
|
# Constructor garbage values fall back to defaults instead of raising.
|
|
288
|
-
|
|
289
|
-
assert
|
|
290
|
-
assert
|
|
288
|
+
ipwhois = IPWhois(timeout="not-a-number", connect_timeout=None)
|
|
289
|
+
assert ipwhois._timeout == 10
|
|
290
|
+
assert ipwhois._connect_timeout == 5
|
|
291
291
|
|
|
292
292
|
|
|
293
293
|
def test_raw_response_includes_success_true() -> None:
|
|
294
294
|
# When the API returns non-JSON (output=xml/csv), the wrapper response
|
|
295
295
|
# must include `success: True` so the documented `if info["success"]`
|
|
296
296
|
# check from the README still works.
|
|
297
|
-
|
|
297
|
+
ipwhois = IPWhois()
|
|
298
298
|
|
|
299
299
|
# Simulate the parsing branch by calling the JSON-decode path with
|
|
300
300
|
# non-JSON input via a tiny direct test of the internal helper:
|
|
@@ -323,7 +323,7 @@ def test_raw_response_includes_success_true() -> None:
|
|
|
323
323
|
|
|
324
324
|
fake = _FakeResp(b"<xml><ip>8.8.8.8</ip></xml>")
|
|
325
325
|
with patch("urllib.request.urlopen", return_value=fake):
|
|
326
|
-
result =
|
|
326
|
+
result = ipwhois.lookup("8.8.8.8", output="xml")
|
|
327
327
|
|
|
328
328
|
assert result["success"] is True
|
|
329
329
|
assert result["raw"] == "<xml><ip>8.8.8.8</ip></xml>"
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to `ipwhois-python` will be documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
-
|
|
8
|
-
## [1.0.0] - 2026-05-08
|
|
9
|
-
|
|
10
|
-
### Added
|
|
11
|
-
|
|
12
|
-
- Initial release.
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""Official Python client for the ipwhois.io IP Geolocation API.
|
|
2
|
-
|
|
3
|
-
See :class:`ipwhois.Client` for usage. The library never raises -- every
|
|
4
|
-
failure comes back inside the response dict with ``success`` set to ``False``
|
|
5
|
-
and a ``message``.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from .client import Client
|
|
9
|
-
|
|
10
|
-
__all__ = ["Client", "__version__"]
|
|
11
|
-
__version__ = Client.VERSION
|
|
File without changes
|
|
File without changes
|
|
File without changes
|