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.
@@ -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.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
- [![PyPI Version](https://img.shields.io/pypi/v/ipwhois-python.svg)](https://pypi.org/project/ipwhois-python/)
57
- [![Python Versions](https://img.shields.io/pypi/pyversions/ipwhois-python.svg)](https://pypi.org/project/ipwhois-python/)
58
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
56
+ [![PyPI Version](https://img.shields.io/pypi/v/ipwhois-python.svg?v=1)](https://pypi.org/project/ipwhois-python/)
57
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ipwhois-python.svg?v=1)](https://pypi.org/project/ipwhois-python/)
58
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg?v=1)](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 `Client` class is used for both plans. The only difference is whether
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 Client
88
+ from ipwhois import IPWhois
89
89
 
90
- free = Client() # Free plan — no API key
91
- paid = Client("YOUR_API_KEY") # Paid plan — with API key
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 Client
99
+ from ipwhois import IPWhois
100
100
 
101
- client = Client() # no API key
101
+ ipwhois = IPWhois() # no API key
102
102
 
103
- info = client.lookup("8.8.8.8")
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 Client
117
+ from ipwhois import IPWhois
118
118
 
119
- client = Client("YOUR_API_KEY") # with API key
119
+ ipwhois = IPWhois("YOUR_API_KEY") # with API key
120
120
 
121
- info = client.lookup("8.8.8.8")
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: `client.lookup()` — works
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
- client = (
153
- Client()
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
- client.lookup("8.8.8.8") # uses all of the above
160
- client.lookup("1.1.1.1", lang="de") # per-call options override defaults
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
- client = (
166
- Client("YOUR_API_KEY")
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
- client.lookup("8.8.8.8") # uses all of the above
173
- client.lookup("1.1.1.1", lang="de") # per-call options override defaults
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 Client
186
+ from ipwhois import IPWhois
187
187
 
188
188
  # Free plan
189
- client = Client(ssl=False)
189
+ ipwhois = IPWhois(ssl=False)
190
190
  ```
191
191
 
192
192
  ```python
193
- from ipwhois import Client
193
+ from ipwhois import IPWhois
194
194
 
195
195
  # Paid plan
196
- client = Client("YOUR_API_KEY", ssl=False)
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 Client
209
+ from ipwhois import IPWhois
210
210
 
211
- client = Client("YOUR_API_KEY")
211
+ ipwhois = IPWhois("YOUR_API_KEY")
212
212
 
213
- results = client.bulk_lookup([
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 = client.lookup("8.8.8.8")
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 = client.lookup("8.8.8.8")
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
- [![PyPI Version](https://img.shields.io/pypi/v/ipwhois-python.svg)](https://pypi.org/project/ipwhois-python/)
4
- [![Python Versions](https://img.shields.io/pypi/pyversions/ipwhois-python.svg)](https://pypi.org/project/ipwhois-python/)
5
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
3
+ [![PyPI Version](https://img.shields.io/pypi/v/ipwhois-python.svg?v=1)](https://pypi.org/project/ipwhois-python/)
4
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ipwhois-python.svg?v=1)](https://pypi.org/project/ipwhois-python/)
5
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg?v=1)](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 `Client` class is used for both plans. The only difference is whether
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 Client
35
+ from ipwhois import IPWhois
36
36
 
37
- free = Client() # Free plan — no API key
38
- paid = Client("YOUR_API_KEY") # Paid plan — with API key
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 Client
46
+ from ipwhois import IPWhois
47
47
 
48
- client = Client() # no API key
48
+ ipwhois = IPWhois() # no API key
49
49
 
50
- info = client.lookup("8.8.8.8")
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 Client
64
+ from ipwhois import IPWhois
65
65
 
66
- client = Client("YOUR_API_KEY") # with API key
66
+ ipwhois = IPWhois("YOUR_API_KEY") # with API key
67
67
 
68
- info = client.lookup("8.8.8.8")
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: `client.lookup()` — works
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
- client = (
100
- Client()
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
- client.lookup("8.8.8.8") # uses all of the above
107
- client.lookup("1.1.1.1", lang="de") # per-call options override defaults
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
- client = (
113
- Client("YOUR_API_KEY")
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
- client.lookup("8.8.8.8") # uses all of the above
120
- client.lookup("1.1.1.1", lang="de") # per-call options override defaults
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 Client
133
+ from ipwhois import IPWhois
134
134
 
135
135
  # Free plan
136
- client = Client(ssl=False)
136
+ ipwhois = IPWhois(ssl=False)
137
137
  ```
138
138
 
139
139
  ```python
140
- from ipwhois import Client
140
+ from ipwhois import IPWhois
141
141
 
142
142
  # Paid plan
143
- client = Client("YOUR_API_KEY", ssl=False)
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 Client
156
+ from ipwhois import IPWhois
157
157
 
158
- client = Client("YOUR_API_KEY")
158
+ ipwhois = IPWhois("YOUR_API_KEY")
159
159
 
160
- results = client.bulk_lookup([
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 = client.lookup("8.8.8.8")
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 = client.lookup("8.8.8.8")
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 Client
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
- client = Client()
13
+ ipwhois = IPWhois()
14
14
 
15
- info = client.lookup("8.8.8.8")
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 = client.lookup()
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 = Client("YOUR_API_KEY")
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 Client
16
+ from ipwhois import IPWhois
17
17
 
18
18
 
19
- client = Client("YOUR_API_KEY")
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 = client.bulk_lookup(ips, lang="en", security=True)
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 Client
11
+ from ipwhois import IPWhois
12
12
 
13
13
 
14
- client = (
15
- Client("YOUR_API_KEY")
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 = client.lookup("8.8.8.8")
34
- cf = client.lookup("1.1.1.1")
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 = client.lookup("8.8.4.4", lang="de")
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.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
- client = ipwhois.Client()
7
- info = client.lookup("8.8.8.8")
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
- client = ipwhois.Client("YOUR_API_KEY")
11
- info = client.lookup("8.8.8.8", lang="en", security=True)
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 = client.bulk_lookup(["8.8.8.8", "1.1.1.1", "208.67.222.222"])
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__ = ["Client"]
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 Client:
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.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) -> "Client":
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
- ) -> "Client":
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) -> "Client":
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) -> "Client":
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) -> "Client":
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) -> "Client":
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) -> "Client":
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:`Client.set_timeout` doesn't blow up
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 Client
9
+ from ipwhois import IPWhois
10
10
 
11
11
 
12
- def _build_url(client: Client, path: str, **options) -> str:
13
- return client._build_url(path, options)
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
- client = Client()
18
- assert _build_url(client, "/8.8.8.8") == "https://ipwho.is/8.8.8.8"
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
- client = Client("TESTKEY")
23
- url = _build_url(client, "/8.8.8.8")
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(Client(), "/").startswith("https://")
31
- assert _build_url(Client("K"), "/").startswith("https://")
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 = Client(ssl=False)
36
- paid = Client("K", ssl=False)
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(Client("K"), "/").startswith("https://")
44
+ assert _build_url(IPWhois("K"), "/").startswith("https://")
45
45
 
46
46
 
47
47
  def test_fields_are_joined_with_commas() -> None:
48
- client = Client("K")
48
+ ipwhois = IPWhois("K")
49
49
  url = _build_url(
50
- client, "/8.8.8.8", fields=["country", "city", "flag.emoji"]
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
- client = Client("K")
59
- url = _build_url(client, "/8.8.8.8", fields="country,city")
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
- client = Client("K")
65
- url = _build_url(client, "/", security=True, rate=True)
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
- client = Client("K")
73
- url = _build_url(client, "/", security=False)
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
- client = Client("K", lang="ru")
80
- url = _build_url(client, "/", lang="en")
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 = Client().lookup("8.8.8.8", lang="klingon")
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 = Client().lookup("8.8.8.8", output="yaml")
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 = Client("K").bulk_lookup([])
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"] * (Client.BULK_LIMIT + 1)
112
- result = Client("K").bulk_lookup(too_many)
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
- client = Client("K")
121
- url = _build_url(client, "/bulk/" + ",".join(["8.8.8.8", "1.1.1.1"]))
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
- client = Client()
127
+ ipwhois = IPWhois()
128
128
 
129
- assert client.set_language("en") is client
130
- assert client.set_fields(["country"]) is client
131
- assert client.set_security(True) is client
132
- assert client.set_rate(False) is client
133
- assert client.set_timeout(5) is client
134
- assert client.set_connect_timeout(2) is client
135
- assert client.set_user_agent("test/1.0") is client
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
- client = Client("K").set_language("de")
140
- url = _build_url(client, "/")
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
- client = Client("K").set_fields(["country", "city"])
147
- url = _build_url(client, "/8.8.8.8")
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
- client = Client("K", lang="ru", security=True)
154
- url = _build_url(client, "/8.8.8.8")
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
- client = Client(user_agent="my-app/2.0")
162
- assert client._user_agent == "my-app/2.0"
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
- client = Client()
167
- assert client._user_agent == f"ipwhois-python/{Client.VERSION}"
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
- client = Client()
173
- url = _build_url(client, "/" + __import__("urllib.parse", fromlist=["quote"]).quote("2c0f:fb50:4003::", safe=""))
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 = Client("K").bulk_lookup("8.8.8.8") # type: ignore[arg-type]
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 = Client("K").bulk_lookup(b"8.8.8.8") # type: ignore[arg-type]
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 = Client("K").bulk_lookup(None) # type: ignore[arg-type]
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 = Client("K").bulk_lookup(42) # type: ignore[arg-type]
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
- client = Client("K")
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 = client.bulk_lookup(empty)
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
- client = Client("K").set_fields("country,city")
237
- url = _build_url(client, "/8.8.8.8")
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
- client = Client().set_fields(["country"]).set_fields(None)
247
- url = client._build_url("/8.8.8.8", {})
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
- client = Client("K").set_fields(42) # type: ignore[arg-type]
254
- url = _build_url(client, "/8.8.8.8")
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
- client = Client()
262
- previous = client._timeout
261
+ ipwhois = IPWhois()
262
+ previous = ipwhois._timeout
263
263
 
264
- client.set_timeout("not a number") # type: ignore[arg-type]
265
- assert client._timeout == previous
264
+ ipwhois.set_timeout("not a number") # type: ignore[arg-type]
265
+ assert ipwhois._timeout == previous
266
266
 
267
- client.set_timeout(None) # type: ignore[arg-type]
268
- assert client._timeout == previous
267
+ ipwhois.set_timeout(None) # type: ignore[arg-type]
268
+ assert ipwhois._timeout == previous
269
269
 
270
- client.set_timeout(-5)
271
- assert client._timeout == previous
270
+ ipwhois.set_timeout(-5)
271
+ assert ipwhois._timeout == previous
272
272
 
273
273
  # Numeric strings are accepted (matches PHP's `(int)` behaviour).
274
- client.set_timeout("15") # type: ignore[arg-type]
275
- assert client._timeout == 15
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
- client = Client()
280
- previous = client._connect_timeout
279
+ ipwhois = IPWhois()
280
+ previous = ipwhois._connect_timeout
281
281
 
282
- client.set_connect_timeout("oops") # type: ignore[arg-type]
283
- assert client._connect_timeout == previous
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
- client = Client(timeout="not-a-number", connect_timeout=None)
289
- assert client._timeout == 10
290
- assert client._connect_timeout == 5
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
- client = Client()
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 = client.lookup("8.8.8.8", output="xml")
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