ipspot 0.5__tar.gz → 0.6__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.
- {ipspot-0.5 → ipspot-0.6}/CHANGELOG.md +16 -1
- {ipspot-0.5 → ipspot-0.6}/PKG-INFO +54 -23
- {ipspot-0.5 → ipspot-0.6}/README.md +35 -19
- {ipspot-0.5 → ipspot-0.6}/SECURITY.md +2 -2
- {ipspot-0.5 → ipspot-0.6}/ipspot/ipv4.py +15 -74
- {ipspot-0.5 → ipspot-0.6}/ipspot/ipv6.py +136 -1
- {ipspot-0.5 → ipspot-0.6}/ipspot/params.py +5 -1
- ipspot-0.6/ipspot/utils.py +132 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot.egg-info/PKG-INFO +54 -23
- {ipspot-0.5 → ipspot-0.6}/setup.py +3 -3
- {ipspot-0.5 → ipspot-0.6}/tests/test_ipv4_api.py +1 -1
- {ipspot-0.5 → ipspot-0.6}/tests/test_ipv6_api.py +32 -0
- ipspot-0.5/ipspot/utils.py +0 -69
- {ipspot-0.5 → ipspot-0.6}/AUTHORS.md +0 -0
- {ipspot-0.5 → ipspot-0.6}/LICENSE +0 -0
- {ipspot-0.5 → ipspot-0.6}/MANIFEST.in +0 -0
- {ipspot-0.5 → ipspot-0.6}/dev-requirements.txt +0 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot/__init__.py +0 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot/__main__.py +0 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot/cli.py +0 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot.egg-info/SOURCES.txt +0 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot.egg-info/dependency_links.txt +0 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot.egg-info/entry_points.txt +0 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot.egg-info/requires.txt +0 -0
- {ipspot-0.5 → ipspot-0.6}/ipspot.egg-info/top_level.txt +0 -0
- {ipspot-0.5 → ipspot-0.6}/requirements.txt +0 -0
- {ipspot-0.5 → ipspot-0.6}/setup.cfg +0 -0
- {ipspot-0.5 → ipspot-0.6}/tests/test_ipv4_functions.py +0 -0
- {ipspot-0.5 → ipspot-0.6}/tests/test_ipv6_functions.py +0 -0
- {ipspot-0.5 → ipspot-0.6}/tests/test_utils.py +0 -0
|
@@ -5,6 +5,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
|
+
## [0.6] - 2025-11-18
|
|
9
|
+
### Added
|
|
10
|
+
- `ForceIPHTTPAdapter` class
|
|
11
|
+
- `_get_json_force_ip` function
|
|
12
|
+
- Support [ifconfig.co](https://ifconfig.co/json) IPv6 API
|
|
13
|
+
- Support [reallyfreegeoip.org](https://reallyfreegeoip.org/json/) IPv6 API
|
|
14
|
+
- Support [myip.la](https://api.myip.la/en?json) IPv6 API
|
|
15
|
+
- Support [freeipapi.com](https://freeipapi.com/api/json) IPv6 API
|
|
16
|
+
### Changed
|
|
17
|
+
- [freeipapi.com](https://freeipapi.com/api/json) IPv4 API bug fixed
|
|
18
|
+
- `README.md` updated
|
|
19
|
+
### Removed
|
|
20
|
+
- `IPv4HTTPAdapter` class
|
|
21
|
+
- `_get_json_ipv4_forced` function
|
|
8
22
|
## [0.5] - 2025-10-17
|
|
9
23
|
### Added
|
|
10
24
|
- `setup-warp` action
|
|
@@ -83,7 +97,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
83
97
|
- `--no-geo` argument
|
|
84
98
|
- Logo
|
|
85
99
|
|
|
86
|
-
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.
|
|
100
|
+
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.6...dev
|
|
101
|
+
[0.6]: https://github.com/openscilab/ipspot/compare/v0.5...v0.6
|
|
87
102
|
[0.5]: https://github.com/openscilab/ipspot/compare/v0.4...v0.5
|
|
88
103
|
[0.4]: https://github.com/openscilab/ipspot/compare/v0.3...v0.4
|
|
89
104
|
[0.3]: https://github.com/openscilab/ipspot/compare/v0.2...v0.3
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipspot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6
|
|
4
4
|
Summary: IPSpot: A Python Tool to Fetch the System's IP Address
|
|
5
5
|
Home-page: https://github.com/openscilab/ipspot
|
|
6
|
-
Download-URL: https://github.com/openscilab/ipspot/tarball/v0.
|
|
6
|
+
Download-URL: https://github.com/openscilab/ipspot/tarball/v0.6
|
|
7
7
|
Author: IPSpot Development Team
|
|
8
8
|
Author-email: ipspot@openscilab.com
|
|
9
9
|
License: MIT
|
|
10
10
|
Project-URL: Source, https://github.com/openscilab/ipspot
|
|
11
11
|
Keywords: ip ipv4 geo geolocation network location ipspot cli
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
@@ -52,6 +52,7 @@ Dynamic: summary
|
|
|
52
52
|
<img src="https://github.com/openscilab/ipspot/raw/main/otherfiles/logo.png" width="350">
|
|
53
53
|
<h1>IPSpot: A Python Tool to Fetch the System's IP Address</h1>
|
|
54
54
|
<br/>
|
|
55
|
+
<a href="https://codecov.io/gh/openscilab/ipspot"><img src="https://codecov.io/gh/openscilab/ipspot/graph/badge.svg?token=XCFKASULS8"></a>
|
|
55
56
|
<a href="https://badge.fury.io/py/ipspot"><img src="https://badge.fury.io/py/ipspot.svg" alt="PyPI version"></a>
|
|
56
57
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/built%20with-Python3-green.svg" alt="built with Python3"></a>
|
|
57
58
|
<a href="https://github.com/openscilab/ipspot"><img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/openscilab/ipspot"></a>
|
|
@@ -102,13 +103,13 @@ Dynamic: summary
|
|
|
102
103
|
## Installation
|
|
103
104
|
|
|
104
105
|
### Source Code
|
|
105
|
-
- Download [Version 0.
|
|
106
|
+
- Download [Version 0.6](https://github.com/openscilab/ipspot/archive/v0.6.zip) or [Latest Source](https://github.com/openscilab/ipspot/archive/dev.zip)
|
|
106
107
|
- `pip install .`
|
|
107
108
|
|
|
108
109
|
### PyPI
|
|
109
110
|
|
|
110
111
|
- Check [Python Packaging User Guide](https://packaging.python.org/installing/)
|
|
111
|
-
- `pip install ipspot==0.
|
|
112
|
+
- `pip install ipspot==0.6`
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
## Usage
|
|
@@ -164,7 +165,7 @@ Dynamic: summary
|
|
|
164
165
|
```console
|
|
165
166
|
> ipspot --version
|
|
166
167
|
|
|
167
|
-
0.
|
|
168
|
+
0.6
|
|
168
169
|
```
|
|
169
170
|
|
|
170
171
|
#### Info
|
|
@@ -179,11 +180,11 @@ Dynamic: summary
|
|
|
179
180
|
|___||_| |____/ | .__/ \___/ \__|
|
|
180
181
|
|_|
|
|
181
182
|
|
|
182
|
-
__ __ ___
|
|
183
|
-
\ \ / / _ / _ \
|
|
184
|
-
\ \ / / (_)| | | | |
|
|
185
|
-
\ V / _ | |_| | _
|
|
186
|
-
\_/ (_) \___/ (_)
|
|
183
|
+
__ __ ___ __
|
|
184
|
+
\ \ / / _ / _ \ / /_
|
|
185
|
+
\ \ / / (_)| | | | | '_ \
|
|
186
|
+
\ V / _ | |_| | _ | (_) |
|
|
187
|
+
\_/ (_) \___/ (_) \___/
|
|
187
188
|
|
|
188
189
|
|
|
189
190
|
|
|
@@ -207,16 +208,31 @@ Private IP:
|
|
|
207
208
|
|
|
208
209
|
Public IP and Location Info:
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
IPv4:
|
|
212
|
+
|
|
213
|
+
API: ipinfo.io
|
|
214
|
+
City: Nuremberg
|
|
215
|
+
Country: Germany
|
|
216
|
+
Country Code: DE
|
|
217
|
+
IP: xx.xx.xx.xx
|
|
218
|
+
Latitude: 49.4527
|
|
219
|
+
Longitude: 11.0783
|
|
220
|
+
Organization: Hetzner Online GmbH
|
|
221
|
+
Region: Bavaria
|
|
222
|
+
Timezone: Europe/Berlin
|
|
223
|
+
|
|
224
|
+
IPv6:
|
|
225
|
+
|
|
226
|
+
API: ip.sb
|
|
227
|
+
City: N/A
|
|
228
|
+
Country: Germany
|
|
229
|
+
Country Code: DE
|
|
230
|
+
IP: xx:xx:xx:xx::xx
|
|
231
|
+
Latitude: 51.2993
|
|
232
|
+
Longitude: 9.491
|
|
233
|
+
Organization: Hetzner Online
|
|
234
|
+
Region: N/A
|
|
235
|
+
Timezone: Europe/Berlin
|
|
220
236
|
```
|
|
221
237
|
|
|
222
238
|
#### IPv4 API
|
|
@@ -264,7 +280,7 @@ Public IP and Location Info:
|
|
|
264
280
|
|
|
265
281
|
#### IPv6 API
|
|
266
282
|
|
|
267
|
-
ℹ️ `ipv6-api` valid choices: [`auto-safe`, `auto`, `ip.sb`, `ident.me`, `tnedi.me`, `ipleak.net`, `my-ip.io`]
|
|
283
|
+
ℹ️ `ipv6-api` valid choices: [`auto-safe`, `auto`, `ip.sb`, `ident.me`, `tnedi.me`, `ipleak.net`, `my-ip.io`, `ifconfig.co`, `reallyfreegeoip.org`, `myip.la`, `freeipapi.com`]
|
|
268
284
|
|
|
269
285
|
ℹ️ The default value: `auto-safe`
|
|
270
286
|
|
|
@@ -360,6 +376,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
|
360
376
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
361
377
|
|
|
362
378
|
## [Unreleased]
|
|
379
|
+
## [0.6] - 2025-11-18
|
|
380
|
+
### Added
|
|
381
|
+
- `ForceIPHTTPAdapter` class
|
|
382
|
+
- `_get_json_force_ip` function
|
|
383
|
+
- Support [ifconfig.co](https://ifconfig.co/json) IPv6 API
|
|
384
|
+
- Support [reallyfreegeoip.org](https://reallyfreegeoip.org/json/) IPv6 API
|
|
385
|
+
- Support [myip.la](https://api.myip.la/en?json) IPv6 API
|
|
386
|
+
- Support [freeipapi.com](https://freeipapi.com/api/json) IPv6 API
|
|
387
|
+
### Changed
|
|
388
|
+
- [freeipapi.com](https://freeipapi.com/api/json) IPv4 API bug fixed
|
|
389
|
+
- `README.md` updated
|
|
390
|
+
### Removed
|
|
391
|
+
- `IPv4HTTPAdapter` class
|
|
392
|
+
- `_get_json_ipv4_forced` function
|
|
363
393
|
## [0.5] - 2025-10-17
|
|
364
394
|
### Added
|
|
365
395
|
- `setup-warp` action
|
|
@@ -438,7 +468,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
438
468
|
- `--no-geo` argument
|
|
439
469
|
- Logo
|
|
440
470
|
|
|
441
|
-
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.
|
|
471
|
+
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.6...dev
|
|
472
|
+
[0.6]: https://github.com/openscilab/ipspot/compare/v0.5...v0.6
|
|
442
473
|
[0.5]: https://github.com/openscilab/ipspot/compare/v0.4...v0.5
|
|
443
474
|
[0.4]: https://github.com/openscilab/ipspot/compare/v0.3...v0.4
|
|
444
475
|
[0.3]: https://github.com/openscilab/ipspot/compare/v0.2...v0.3
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<img src="https://github.com/openscilab/ipspot/raw/main/otherfiles/logo.png" width="350">
|
|
3
3
|
<h1>IPSpot: A Python Tool to Fetch the System's IP Address</h1>
|
|
4
4
|
<br/>
|
|
5
|
+
<a href="https://codecov.io/gh/openscilab/ipspot"><img src="https://codecov.io/gh/openscilab/ipspot/graph/badge.svg?token=XCFKASULS8"></a>
|
|
5
6
|
<a href="https://badge.fury.io/py/ipspot"><img src="https://badge.fury.io/py/ipspot.svg" alt="PyPI version"></a>
|
|
6
7
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/built%20with-Python3-green.svg" alt="built with Python3"></a>
|
|
7
8
|
<a href="https://github.com/openscilab/ipspot"><img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/openscilab/ipspot"></a>
|
|
@@ -52,13 +53,13 @@
|
|
|
52
53
|
## Installation
|
|
53
54
|
|
|
54
55
|
### Source Code
|
|
55
|
-
- Download [Version 0.
|
|
56
|
+
- Download [Version 0.6](https://github.com/openscilab/ipspot/archive/v0.6.zip) or [Latest Source](https://github.com/openscilab/ipspot/archive/dev.zip)
|
|
56
57
|
- `pip install .`
|
|
57
58
|
|
|
58
59
|
### PyPI
|
|
59
60
|
|
|
60
61
|
- Check [Python Packaging User Guide](https://packaging.python.org/installing/)
|
|
61
|
-
- `pip install ipspot==0.
|
|
62
|
+
- `pip install ipspot==0.6`
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
## Usage
|
|
@@ -114,7 +115,7 @@
|
|
|
114
115
|
```console
|
|
115
116
|
> ipspot --version
|
|
116
117
|
|
|
117
|
-
0.
|
|
118
|
+
0.6
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
#### Info
|
|
@@ -129,11 +130,11 @@
|
|
|
129
130
|
|___||_| |____/ | .__/ \___/ \__|
|
|
130
131
|
|_|
|
|
131
132
|
|
|
132
|
-
__ __ ___
|
|
133
|
-
\ \ / / _ / _ \
|
|
134
|
-
\ \ / / (_)| | | | |
|
|
135
|
-
\ V / _ | |_| | _
|
|
136
|
-
\_/ (_) \___/ (_)
|
|
133
|
+
__ __ ___ __
|
|
134
|
+
\ \ / / _ / _ \ / /_
|
|
135
|
+
\ \ / / (_)| | | | | '_ \
|
|
136
|
+
\ V / _ | |_| | _ | (_) |
|
|
137
|
+
\_/ (_) \___/ (_) \___/
|
|
137
138
|
|
|
138
139
|
|
|
139
140
|
|
|
@@ -157,16 +158,31 @@ Private IP:
|
|
|
157
158
|
|
|
158
159
|
Public IP and Location Info:
|
|
159
160
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
161
|
+
IPv4:
|
|
162
|
+
|
|
163
|
+
API: ipinfo.io
|
|
164
|
+
City: Nuremberg
|
|
165
|
+
Country: Germany
|
|
166
|
+
Country Code: DE
|
|
167
|
+
IP: xx.xx.xx.xx
|
|
168
|
+
Latitude: 49.4527
|
|
169
|
+
Longitude: 11.0783
|
|
170
|
+
Organization: Hetzner Online GmbH
|
|
171
|
+
Region: Bavaria
|
|
172
|
+
Timezone: Europe/Berlin
|
|
173
|
+
|
|
174
|
+
IPv6:
|
|
175
|
+
|
|
176
|
+
API: ip.sb
|
|
177
|
+
City: N/A
|
|
178
|
+
Country: Germany
|
|
179
|
+
Country Code: DE
|
|
180
|
+
IP: xx:xx:xx:xx::xx
|
|
181
|
+
Latitude: 51.2993
|
|
182
|
+
Longitude: 9.491
|
|
183
|
+
Organization: Hetzner Online
|
|
184
|
+
Region: N/A
|
|
185
|
+
Timezone: Europe/Berlin
|
|
170
186
|
```
|
|
171
187
|
|
|
172
188
|
#### IPv4 API
|
|
@@ -214,7 +230,7 @@ Public IP and Location Info:
|
|
|
214
230
|
|
|
215
231
|
#### IPv6 API
|
|
216
232
|
|
|
217
|
-
ℹ️ `ipv6-api` valid choices: [`auto-safe`, `auto`, `ip.sb`, `ident.me`, `tnedi.me`, `ipleak.net`, `my-ip.io`]
|
|
233
|
+
ℹ️ `ipv6-api` valid choices: [`auto-safe`, `auto`, `ip.sb`, `ident.me`, `tnedi.me`, `ipleak.net`, `my-ip.io`, `ifconfig.co`, `reallyfreegeoip.org`, `myip.la`, `freeipapi.com`]
|
|
218
234
|
|
|
219
235
|
ℹ️ The default value: `auto-safe`
|
|
220
236
|
|
|
@@ -3,69 +3,9 @@
|
|
|
3
3
|
import ipaddress
|
|
4
4
|
import socket
|
|
5
5
|
from typing import Union, Dict, List, Tuple
|
|
6
|
-
import
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from .utils import is_loopback, _get_json_standard, _attempt_with_retries
|
|
10
|
-
from .params import REQUEST_HEADERS, IPv4API
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class IPv4HTTPAdapter(HTTPAdapter):
|
|
14
|
-
"""A custom HTTPAdapter that enforces the use of IPv4 for DNS resolution during HTTP(S) requests using the requests library."""
|
|
15
|
-
|
|
16
|
-
def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **kwargs: dict) -> None:
|
|
17
|
-
"""
|
|
18
|
-
Initialize the connection pool manager using a temporary override of socket.getaddrinfo to ensure only IPv4 addresses are used.
|
|
19
|
-
|
|
20
|
-
:param connections: the number of connection pools to cache
|
|
21
|
-
:param maxsize: the maximum number of connections to save in the pool
|
|
22
|
-
:param block: whether the connections should block when reaching the max size
|
|
23
|
-
:param kwargs: additional keyword arguments for the PoolManager
|
|
24
|
-
"""
|
|
25
|
-
self.poolmanager = PoolManager(
|
|
26
|
-
num_pools=connections,
|
|
27
|
-
maxsize=maxsize,
|
|
28
|
-
block=block,
|
|
29
|
-
socket_options=self._ipv4_socket_options(),
|
|
30
|
-
**kwargs
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
def _ipv4_socket_options(self) -> list:
|
|
34
|
-
"""
|
|
35
|
-
Temporarily patches socket.getaddrinfo to filter only IPv4 addresses (AF_INET).
|
|
36
|
-
|
|
37
|
-
:return: an empty list of socket options; DNS patching occurs here
|
|
38
|
-
"""
|
|
39
|
-
original_getaddrinfo = socket.getaddrinfo
|
|
40
|
-
|
|
41
|
-
def ipv4_only_getaddrinfo(*args: list, **kwargs: dict) -> List[Tuple]:
|
|
42
|
-
results = original_getaddrinfo(*args, **kwargs)
|
|
43
|
-
return [res for res in results if res[0] == socket.AF_INET]
|
|
44
|
-
|
|
45
|
-
self._original_getaddrinfo = socket.getaddrinfo
|
|
46
|
-
socket.getaddrinfo = ipv4_only_getaddrinfo
|
|
47
|
-
|
|
48
|
-
return []
|
|
49
|
-
|
|
50
|
-
def __del__(self) -> None:
|
|
51
|
-
"""Restores the original socket.getaddrinfo function upon adapter deletion."""
|
|
52
|
-
if hasattr(self, "_original_getaddrinfo"):
|
|
53
|
-
socket.getaddrinfo = self._original_getaddrinfo
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _get_json_ipv4_forced(url: str, timeout: Union[float, Tuple[float, float]]) -> dict:
|
|
57
|
-
"""
|
|
58
|
-
Send GET request with forced IPv4 using IPv4HTTPAdapter that returns JSON response.
|
|
59
|
-
|
|
60
|
-
:param url: API url
|
|
61
|
-
:param timeout: timeout value for API
|
|
62
|
-
"""
|
|
63
|
-
with requests.Session() as session:
|
|
64
|
-
session.mount("http://", IPv4HTTPAdapter())
|
|
65
|
-
session.mount("https://", IPv4HTTPAdapter())
|
|
66
|
-
response = session.get(url, headers=REQUEST_HEADERS, timeout=timeout)
|
|
67
|
-
response.raise_for_status()
|
|
68
|
-
return response.json()
|
|
6
|
+
from .utils import is_loopback, _attempt_with_retries
|
|
7
|
+
from .utils import _get_json_standard, _get_json_force_ip
|
|
8
|
+
from .params import IPv4API
|
|
69
9
|
|
|
70
10
|
|
|
71
11
|
def is_ipv4(ip: str) -> bool:
|
|
@@ -189,7 +129,7 @@ def _ifconfig_co_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]
|
|
|
189
129
|
:param timeout: timeout value for API
|
|
190
130
|
"""
|
|
191
131
|
try:
|
|
192
|
-
data =
|
|
132
|
+
data = _get_json_force_ip(url="https://ifconfig.co/json", timeout=timeout, version="ipv4")
|
|
193
133
|
result = {"status": True, "data": {"ip": data["ip"], "api": "ifconfig.co"}}
|
|
194
134
|
if geo:
|
|
195
135
|
geo_data = {
|
|
@@ -217,7 +157,7 @@ def _ipapi_co_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
217
157
|
:param timeout: timeout value for API
|
|
218
158
|
"""
|
|
219
159
|
try:
|
|
220
|
-
data =
|
|
160
|
+
data = _get_json_force_ip(url="https://ipapi.co/json/", timeout=timeout, version="ipv4")
|
|
221
161
|
result = {"status": True, "data": {"ip": data["ip"], "api": "ipapi.co"}}
|
|
222
162
|
if geo:
|
|
223
163
|
geo_data = {
|
|
@@ -245,7 +185,7 @@ def _ip_api_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
245
185
|
:param timeout: timeout value for API
|
|
246
186
|
"""
|
|
247
187
|
try:
|
|
248
|
-
data =
|
|
188
|
+
data = _get_json_force_ip(url="http://ip-api.com/json/", timeout=timeout, version="ipv4")
|
|
249
189
|
if data.get("status") != "success":
|
|
250
190
|
return {"status": False, "error": "ip-api lookup failed"}
|
|
251
191
|
result = {"status": True, "data": {"ip": data["query"], "api": "ip-api.com"}}
|
|
@@ -275,7 +215,7 @@ def _ipinfo_io_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
275
215
|
:param timeout: timeout value for API
|
|
276
216
|
"""
|
|
277
217
|
try:
|
|
278
|
-
data =
|
|
218
|
+
data = _get_json_force_ip(url="https://ipinfo.io/json", timeout=timeout, version="ipv4")
|
|
279
219
|
result = {"status": True, "data": {"ip": data["ip"], "api": "ipinfo.io"}}
|
|
280
220
|
if geo:
|
|
281
221
|
loc = data.get("loc", "").split(",")
|
|
@@ -304,7 +244,7 @@ def _reallyfreegeoip_org_ipv4(geo: bool=False, timeout: Union[float, Tuple[float
|
|
|
304
244
|
:param timeout: timeout value for API
|
|
305
245
|
"""
|
|
306
246
|
try:
|
|
307
|
-
data =
|
|
247
|
+
data = _get_json_force_ip(url="https://reallyfreegeoip.org/json/", timeout=timeout, version="ipv4")
|
|
308
248
|
result = {"status": True, "data": {"ip": data["ip"], "api": "reallyfreegeoip.org"}}
|
|
309
249
|
if geo:
|
|
310
250
|
geo_data = {
|
|
@@ -388,7 +328,7 @@ def _myip_la_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5
|
|
|
388
328
|
:param timeout: timeout value for API
|
|
389
329
|
"""
|
|
390
330
|
try:
|
|
391
|
-
data =
|
|
331
|
+
data = _get_json_force_ip(url="https://api.myip.la/en?json", timeout=timeout, version="ipv4")
|
|
392
332
|
result = {"status": True, "data": {"ip": data["ip"], "api": "myip.la"}}
|
|
393
333
|
if geo:
|
|
394
334
|
loc = data.get("location", {})
|
|
@@ -417,8 +357,9 @@ def _freeipapi_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, floa
|
|
|
417
357
|
:param timeout: timeout value for API
|
|
418
358
|
"""
|
|
419
359
|
try:
|
|
420
|
-
data =
|
|
360
|
+
data = _get_json_force_ip(url="https://freeipapi.com/api/json", timeout=timeout, version="ipv4")
|
|
421
361
|
result = {"status": True, "data": {"ip": data["ipAddress"], "api": "freeipapi.com"}}
|
|
362
|
+
tzs = data.get("timeZones", [])
|
|
422
363
|
if geo:
|
|
423
364
|
geo_data = {
|
|
424
365
|
"city": data.get("cityName"),
|
|
@@ -427,8 +368,8 @@ def _freeipapi_com_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, floa
|
|
|
427
368
|
"country_code": data.get("countryCode"),
|
|
428
369
|
"latitude": data.get("latitude"),
|
|
429
370
|
"longitude": data.get("longitude"),
|
|
430
|
-
"organization":
|
|
431
|
-
"timezone":
|
|
371
|
+
"organization": data.get("asnOrganization"),
|
|
372
|
+
"timezone": tzs[0] if len(tzs) > 0 else None
|
|
432
373
|
}
|
|
433
374
|
result["data"].update(geo_data)
|
|
434
375
|
return result
|
|
@@ -445,7 +386,7 @@ def _ipquery_io_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
445
386
|
:param timeout: timeout value for API
|
|
446
387
|
"""
|
|
447
388
|
try:
|
|
448
|
-
data =
|
|
389
|
+
data = _get_json_force_ip(url="https://api.ipquery.io/?format=json", timeout=timeout, version="ipv4")
|
|
449
390
|
result = {"status": True, "data": {"ip": data["ip"], "api": "ipquery.io"}}
|
|
450
391
|
if geo:
|
|
451
392
|
loc = data.get("location", {})
|
|
@@ -475,7 +416,7 @@ def _ipwho_is_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]=5
|
|
|
475
416
|
:param timeout: timeout value for API
|
|
476
417
|
"""
|
|
477
418
|
try:
|
|
478
|
-
data =
|
|
419
|
+
data = _get_json_force_ip(url="https://ipwho.is", timeout=timeout, version="ipv4")
|
|
479
420
|
result = {"status": True, "data": {"ip": data["ip"], "api": "ipwho.is"}}
|
|
480
421
|
if geo:
|
|
481
422
|
connection = data.get("connection", {})
|
|
@@ -4,7 +4,8 @@ import ipaddress
|
|
|
4
4
|
import socket
|
|
5
5
|
from typing import Union, Dict, List, Tuple
|
|
6
6
|
from .params import IPv6API
|
|
7
|
-
from .utils import is_loopback,
|
|
7
|
+
from .utils import is_loopback, _attempt_with_retries
|
|
8
|
+
from .utils import _get_json_standard, _get_json_force_ip
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def is_ipv6(ip: str) -> bool:
|
|
@@ -176,6 +177,120 @@ def _my_ip_io_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
|
176
177
|
return {"status": False, "error": str(e)}
|
|
177
178
|
|
|
178
179
|
|
|
180
|
+
def _ifconfig_co_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]] # very low rate limit
|
|
181
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
182
|
+
"""
|
|
183
|
+
Get public IP and geolocation using ifconfig.co.
|
|
184
|
+
|
|
185
|
+
:param geo: geolocation flag
|
|
186
|
+
:param timeout: timeout value for API
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
data = _get_json_force_ip(url="https://ifconfig.co/json", timeout=timeout, version="ipv6")
|
|
190
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "ifconfig.co"}}
|
|
191
|
+
if geo:
|
|
192
|
+
geo_data = {
|
|
193
|
+
"city": data.get("city"),
|
|
194
|
+
"region": data.get("region_name"),
|
|
195
|
+
"country": data.get("country"),
|
|
196
|
+
"country_code": data.get("country_iso"),
|
|
197
|
+
"latitude": data.get("latitude"),
|
|
198
|
+
"longitude": data.get("longitude"),
|
|
199
|
+
"organization": data.get("asn_org"),
|
|
200
|
+
"timezone": data.get("time_zone")
|
|
201
|
+
}
|
|
202
|
+
result["data"].update(geo_data)
|
|
203
|
+
return result
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return {"status": False, "error": str(e)}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _reallyfreegeoip_org_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
209
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
210
|
+
"""
|
|
211
|
+
Get public IP and geolocation using reallyfreegeoip.org.
|
|
212
|
+
|
|
213
|
+
:param geo: geolocation flag
|
|
214
|
+
:param timeout: timeout value for API
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
data = _get_json_force_ip(url="https://reallyfreegeoip.org/json/", timeout=timeout, version="ipv6")
|
|
218
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "reallyfreegeoip.org"}}
|
|
219
|
+
if geo:
|
|
220
|
+
geo_data = {
|
|
221
|
+
"city": data.get("city"),
|
|
222
|
+
"region": data.get("region_name"),
|
|
223
|
+
"country": data.get("country_name"),
|
|
224
|
+
"country_code": data.get("country_code"),
|
|
225
|
+
"latitude": data.get("latitude"),
|
|
226
|
+
"longitude": data.get("longitude"),
|
|
227
|
+
"organization": None, # does not provide organization
|
|
228
|
+
"timezone": data.get("time_zone")
|
|
229
|
+
}
|
|
230
|
+
result["data"].update(geo_data)
|
|
231
|
+
return result
|
|
232
|
+
except Exception as e:
|
|
233
|
+
return {"status": False, "error": str(e)}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _myip_la_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
237
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
238
|
+
"""
|
|
239
|
+
Get public IP and geolocation using myip.la.
|
|
240
|
+
|
|
241
|
+
:param geo: geolocation flag
|
|
242
|
+
:param timeout: timeout value for API
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
data = _get_json_force_ip(url="https://api.myip.la/en?json", timeout=timeout, version="ipv6")
|
|
246
|
+
result = {"status": True, "data": {"ip": data["ip"], "api": "myip.la"}}
|
|
247
|
+
if geo:
|
|
248
|
+
loc = data.get("location", {})
|
|
249
|
+
geo_data = {
|
|
250
|
+
"city": loc.get("city"),
|
|
251
|
+
"region": loc.get("province"),
|
|
252
|
+
"country": loc.get("country_name"),
|
|
253
|
+
"country_code": loc.get("country_code"),
|
|
254
|
+
"latitude": float(loc.get("latitude")) if loc.get("latitude") else None,
|
|
255
|
+
"longitude": float(loc.get("longitude")) if loc.get("longitude") else None,
|
|
256
|
+
"organization": None,
|
|
257
|
+
"timezone": None
|
|
258
|
+
}
|
|
259
|
+
result["data"].update(geo_data)
|
|
260
|
+
return result
|
|
261
|
+
except Exception as e:
|
|
262
|
+
return {"status": False, "error": str(e)}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _freeipapi_com_ipv6(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
266
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
267
|
+
"""
|
|
268
|
+
Get public IP and geolocation using freeipapi.com.
|
|
269
|
+
|
|
270
|
+
:param geo: geolocation flag
|
|
271
|
+
:param timeout: timeout value for API
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
data = _get_json_force_ip(url="https://free.freeipapi.com/api/json", timeout=timeout, version="ipv6")
|
|
275
|
+
result = {"status": True, "data": {"ip": data["ipAddress"], "api": "freeipapi.com"}}
|
|
276
|
+
tzs = data.get("timeZones", [])
|
|
277
|
+
if geo:
|
|
278
|
+
geo_data = {
|
|
279
|
+
"city": data.get("cityName"),
|
|
280
|
+
"region": data.get("regionName"),
|
|
281
|
+
"country": data.get("countryName"),
|
|
282
|
+
"country_code": data.get("countryCode"),
|
|
283
|
+
"latitude": data.get("latitude"),
|
|
284
|
+
"longitude": data.get("longitude"),
|
|
285
|
+
"organization": data.get("asnOrganization"),
|
|
286
|
+
"timezone": tzs[0] if len(tzs) > 0 else None
|
|
287
|
+
}
|
|
288
|
+
result["data"].update(geo_data)
|
|
289
|
+
return result
|
|
290
|
+
except Exception as e:
|
|
291
|
+
return {"status": False, "error": str(e)}
|
|
292
|
+
|
|
293
|
+
|
|
179
294
|
IPV6_API_MAP = {
|
|
180
295
|
IPv6API.IP_SB: {
|
|
181
296
|
"thread_safe": True,
|
|
@@ -202,6 +317,26 @@ IPV6_API_MAP = {
|
|
|
202
317
|
"geo": True,
|
|
203
318
|
"function": _my_ip_io_ipv6
|
|
204
319
|
},
|
|
320
|
+
IPv6API.IFCONFIG_CO: {
|
|
321
|
+
"thread_safe": False,
|
|
322
|
+
"geo": True,
|
|
323
|
+
"function": _ifconfig_co_ipv6
|
|
324
|
+
},
|
|
325
|
+
IPv6API.REALLYFREEGEOIP_ORG: {
|
|
326
|
+
"thread_safe": False,
|
|
327
|
+
"geo": True,
|
|
328
|
+
"function": _reallyfreegeoip_org_ipv6
|
|
329
|
+
},
|
|
330
|
+
IPv6API.MYIP_LA: {
|
|
331
|
+
"thread_safe": False,
|
|
332
|
+
"geo": True,
|
|
333
|
+
"function": _myip_la_ipv6
|
|
334
|
+
},
|
|
335
|
+
IPv6API.FREEIPAPI_COM: {
|
|
336
|
+
"thread_safe": False,
|
|
337
|
+
"geo": True,
|
|
338
|
+
"function": _freeipapi_com_ipv6
|
|
339
|
+
}
|
|
205
340
|
}
|
|
206
341
|
|
|
207
342
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""ipspot params."""
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
|
-
IPSPOT_VERSION = "0.
|
|
5
|
+
IPSPOT_VERSION = "0.6"
|
|
6
6
|
|
|
7
7
|
IPSPOT_OVERVIEW = '''
|
|
8
8
|
IPSpot is a Python library for retrieving the current system's IP address and location information.
|
|
@@ -50,6 +50,10 @@ class IPv6API(Enum):
|
|
|
50
50
|
TNEDI_ME = "tnedi.me"
|
|
51
51
|
IPLEAK_NET = "ipleak.net"
|
|
52
52
|
MY_IP_IO = "my-ip.io"
|
|
53
|
+
IFCONFIG_CO = "ifconfig.co"
|
|
54
|
+
REALLYFREEGEOIP_ORG = "reallyfreegeoip.org"
|
|
55
|
+
MYIP_LA = "myip.la"
|
|
56
|
+
FREEIPAPI_COM = "freeipapi.com"
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
PARAMETERS_NAME_MAP = {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ipspot utils."""
|
|
3
|
+
import time
|
|
4
|
+
import ipaddress
|
|
5
|
+
import socket
|
|
6
|
+
import requests
|
|
7
|
+
from requests.adapters import HTTPAdapter
|
|
8
|
+
from typing import Callable, Dict
|
|
9
|
+
from typing import Union, Tuple, Any, List
|
|
10
|
+
from .params import REQUEST_HEADERS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ForceIPHTTPAdapter(HTTPAdapter):
|
|
14
|
+
"""A custom HTTPAdapter that enforces IPv4 or IPv6 DNS resolution for HTTP(S) requests."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, version: str = "ipv4", *args: list, **kwargs: dict) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Initialize the adapter with the desired IP version.
|
|
19
|
+
|
|
20
|
+
:param version: 'ipv4' or 'ipv6' to select address family
|
|
21
|
+
:param args: additional list arguments for the HTTPAdapter
|
|
22
|
+
:param kwargs: additional keyword arguments for the HTTPAdapter
|
|
23
|
+
"""
|
|
24
|
+
self.version = version.lower()
|
|
25
|
+
if self.version not in ("ipv4", "ipv6"):
|
|
26
|
+
raise ValueError("version must be either 'ipv4' or 'ipv6'")
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
def send(self, *args: list, **kwargs: dict) -> Any:
|
|
30
|
+
"""
|
|
31
|
+
Override send method to apply the monkey patch only during the request.
|
|
32
|
+
|
|
33
|
+
:param args: additional list arguments for the send method
|
|
34
|
+
:param kwargs: additional keyword arguments for the send method
|
|
35
|
+
"""
|
|
36
|
+
family = socket.AF_INET if self.version == "ipv4" else socket.AF_INET6
|
|
37
|
+
original_getaddrinfo = socket.getaddrinfo
|
|
38
|
+
|
|
39
|
+
def filtered_getaddrinfo(*gargs: list, **gkwargs: dict) -> List[Tuple]:
|
|
40
|
+
"""
|
|
41
|
+
Filter getaddrinfo.
|
|
42
|
+
|
|
43
|
+
:param gargs: additional list arguments for the original_getaddrinfo function
|
|
44
|
+
:param gkwargs: additional keyword arguments for the original_getaddrinfo function
|
|
45
|
+
"""
|
|
46
|
+
results = original_getaddrinfo(*gargs, **gkwargs)
|
|
47
|
+
return [res for res in results if res[0] == family]
|
|
48
|
+
|
|
49
|
+
socket.getaddrinfo = filtered_getaddrinfo
|
|
50
|
+
try:
|
|
51
|
+
response = super().send(*args, **kwargs)
|
|
52
|
+
finally:
|
|
53
|
+
socket.getaddrinfo = original_getaddrinfo
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_json_force_ip(url: str, timeout: Union[float, Tuple[float, float]],
|
|
58
|
+
version: str = "ipv4") -> dict:
|
|
59
|
+
"""
|
|
60
|
+
Send GET request with forced IPv4/IPv6 using ForceIPHTTPAdapter that returns JSON response.
|
|
61
|
+
|
|
62
|
+
:param url: API url
|
|
63
|
+
:param timeout: timeout value for API
|
|
64
|
+
:param version: 'ipv4' or 'ipv6' to select address family
|
|
65
|
+
"""
|
|
66
|
+
with requests.Session() as session:
|
|
67
|
+
session.mount("http://", ForceIPHTTPAdapter(version=version))
|
|
68
|
+
session.mount("https://", ForceIPHTTPAdapter(version=version))
|
|
69
|
+
response = session.get(url, headers=REQUEST_HEADERS, timeout=timeout)
|
|
70
|
+
response.raise_for_status()
|
|
71
|
+
return response.json()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _attempt_with_retries(
|
|
75
|
+
func: Callable,
|
|
76
|
+
max_retries: int,
|
|
77
|
+
retry_delay: float, **kwargs: dict) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
78
|
+
"""
|
|
79
|
+
Attempt a function call with retries and delay.
|
|
80
|
+
|
|
81
|
+
:param func: function to execute
|
|
82
|
+
:param max_retries: number of retries
|
|
83
|
+
:param retry_delay: delay between retries (in seconds)
|
|
84
|
+
:param kwargs: keyword arguments to pass to the function
|
|
85
|
+
"""
|
|
86
|
+
max_retries = max(0, max_retries)
|
|
87
|
+
result = {"status": False, "error": ""}
|
|
88
|
+
for attempt in range(max_retries + 1):
|
|
89
|
+
result = func(**kwargs)
|
|
90
|
+
if result["status"]:
|
|
91
|
+
break
|
|
92
|
+
time.sleep(retry_delay)
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_loopback(ip: str) -> bool:
|
|
97
|
+
"""
|
|
98
|
+
Check if the given input IP is a loopback address.
|
|
99
|
+
|
|
100
|
+
:param ip: input IP
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
ip_object = ipaddress.ip_address(ip)
|
|
104
|
+
return ip_object.is_loopback
|
|
105
|
+
except Exception:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _filter_parameter(parameter: Any) -> Any:
|
|
110
|
+
"""
|
|
111
|
+
Filter input parameter.
|
|
112
|
+
|
|
113
|
+
:param parameter: input parameter
|
|
114
|
+
"""
|
|
115
|
+
if parameter is None:
|
|
116
|
+
return "N/A"
|
|
117
|
+
if isinstance(parameter, str) and len(parameter.strip()) == 0:
|
|
118
|
+
return "N/A"
|
|
119
|
+
return parameter
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_json_standard(url: str, timeout: Union[float, Tuple[float, float]]) -> dict:
|
|
123
|
+
"""
|
|
124
|
+
Send standard GET request that returns JSON response.
|
|
125
|
+
|
|
126
|
+
:param url: API url
|
|
127
|
+
:param timeout: timeout value for API
|
|
128
|
+
"""
|
|
129
|
+
with requests.Session() as session:
|
|
130
|
+
response = session.get(url, headers=REQUEST_HEADERS, timeout=timeout)
|
|
131
|
+
response.raise_for_status()
|
|
132
|
+
return response.json()
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipspot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6
|
|
4
4
|
Summary: IPSpot: A Python Tool to Fetch the System's IP Address
|
|
5
5
|
Home-page: https://github.com/openscilab/ipspot
|
|
6
|
-
Download-URL: https://github.com/openscilab/ipspot/tarball/v0.
|
|
6
|
+
Download-URL: https://github.com/openscilab/ipspot/tarball/v0.6
|
|
7
7
|
Author: IPSpot Development Team
|
|
8
8
|
Author-email: ipspot@openscilab.com
|
|
9
9
|
License: MIT
|
|
10
10
|
Project-URL: Source, https://github.com/openscilab/ipspot
|
|
11
11
|
Keywords: ip ipv4 geo geolocation network location ipspot cli
|
|
12
|
-
Classifier: Development Status ::
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Natural Language :: English
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
@@ -52,6 +52,7 @@ Dynamic: summary
|
|
|
52
52
|
<img src="https://github.com/openscilab/ipspot/raw/main/otherfiles/logo.png" width="350">
|
|
53
53
|
<h1>IPSpot: A Python Tool to Fetch the System's IP Address</h1>
|
|
54
54
|
<br/>
|
|
55
|
+
<a href="https://codecov.io/gh/openscilab/ipspot"><img src="https://codecov.io/gh/openscilab/ipspot/graph/badge.svg?token=XCFKASULS8"></a>
|
|
55
56
|
<a href="https://badge.fury.io/py/ipspot"><img src="https://badge.fury.io/py/ipspot.svg" alt="PyPI version"></a>
|
|
56
57
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/built%20with-Python3-green.svg" alt="built with Python3"></a>
|
|
57
58
|
<a href="https://github.com/openscilab/ipspot"><img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/openscilab/ipspot"></a>
|
|
@@ -102,13 +103,13 @@ Dynamic: summary
|
|
|
102
103
|
## Installation
|
|
103
104
|
|
|
104
105
|
### Source Code
|
|
105
|
-
- Download [Version 0.
|
|
106
|
+
- Download [Version 0.6](https://github.com/openscilab/ipspot/archive/v0.6.zip) or [Latest Source](https://github.com/openscilab/ipspot/archive/dev.zip)
|
|
106
107
|
- `pip install .`
|
|
107
108
|
|
|
108
109
|
### PyPI
|
|
109
110
|
|
|
110
111
|
- Check [Python Packaging User Guide](https://packaging.python.org/installing/)
|
|
111
|
-
- `pip install ipspot==0.
|
|
112
|
+
- `pip install ipspot==0.6`
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
## Usage
|
|
@@ -164,7 +165,7 @@ Dynamic: summary
|
|
|
164
165
|
```console
|
|
165
166
|
> ipspot --version
|
|
166
167
|
|
|
167
|
-
0.
|
|
168
|
+
0.6
|
|
168
169
|
```
|
|
169
170
|
|
|
170
171
|
#### Info
|
|
@@ -179,11 +180,11 @@ Dynamic: summary
|
|
|
179
180
|
|___||_| |____/ | .__/ \___/ \__|
|
|
180
181
|
|_|
|
|
181
182
|
|
|
182
|
-
__ __ ___
|
|
183
|
-
\ \ / / _ / _ \
|
|
184
|
-
\ \ / / (_)| | | | |
|
|
185
|
-
\ V / _ | |_| | _
|
|
186
|
-
\_/ (_) \___/ (_)
|
|
183
|
+
__ __ ___ __
|
|
184
|
+
\ \ / / _ / _ \ / /_
|
|
185
|
+
\ \ / / (_)| | | | | '_ \
|
|
186
|
+
\ V / _ | |_| | _ | (_) |
|
|
187
|
+
\_/ (_) \___/ (_) \___/
|
|
187
188
|
|
|
188
189
|
|
|
189
190
|
|
|
@@ -207,16 +208,31 @@ Private IP:
|
|
|
207
208
|
|
|
208
209
|
Public IP and Location Info:
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
211
|
+
IPv4:
|
|
212
|
+
|
|
213
|
+
API: ipinfo.io
|
|
214
|
+
City: Nuremberg
|
|
215
|
+
Country: Germany
|
|
216
|
+
Country Code: DE
|
|
217
|
+
IP: xx.xx.xx.xx
|
|
218
|
+
Latitude: 49.4527
|
|
219
|
+
Longitude: 11.0783
|
|
220
|
+
Organization: Hetzner Online GmbH
|
|
221
|
+
Region: Bavaria
|
|
222
|
+
Timezone: Europe/Berlin
|
|
223
|
+
|
|
224
|
+
IPv6:
|
|
225
|
+
|
|
226
|
+
API: ip.sb
|
|
227
|
+
City: N/A
|
|
228
|
+
Country: Germany
|
|
229
|
+
Country Code: DE
|
|
230
|
+
IP: xx:xx:xx:xx::xx
|
|
231
|
+
Latitude: 51.2993
|
|
232
|
+
Longitude: 9.491
|
|
233
|
+
Organization: Hetzner Online
|
|
234
|
+
Region: N/A
|
|
235
|
+
Timezone: Europe/Berlin
|
|
220
236
|
```
|
|
221
237
|
|
|
222
238
|
#### IPv4 API
|
|
@@ -264,7 +280,7 @@ Public IP and Location Info:
|
|
|
264
280
|
|
|
265
281
|
#### IPv6 API
|
|
266
282
|
|
|
267
|
-
ℹ️ `ipv6-api` valid choices: [`auto-safe`, `auto`, `ip.sb`, `ident.me`, `tnedi.me`, `ipleak.net`, `my-ip.io`]
|
|
283
|
+
ℹ️ `ipv6-api` valid choices: [`auto-safe`, `auto`, `ip.sb`, `ident.me`, `tnedi.me`, `ipleak.net`, `my-ip.io`, `ifconfig.co`, `reallyfreegeoip.org`, `myip.la`, `freeipapi.com`]
|
|
268
284
|
|
|
269
285
|
ℹ️ The default value: `auto-safe`
|
|
270
286
|
|
|
@@ -360,6 +376,20 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
|
360
376
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
361
377
|
|
|
362
378
|
## [Unreleased]
|
|
379
|
+
## [0.6] - 2025-11-18
|
|
380
|
+
### Added
|
|
381
|
+
- `ForceIPHTTPAdapter` class
|
|
382
|
+
- `_get_json_force_ip` function
|
|
383
|
+
- Support [ifconfig.co](https://ifconfig.co/json) IPv6 API
|
|
384
|
+
- Support [reallyfreegeoip.org](https://reallyfreegeoip.org/json/) IPv6 API
|
|
385
|
+
- Support [myip.la](https://api.myip.la/en?json) IPv6 API
|
|
386
|
+
- Support [freeipapi.com](https://freeipapi.com/api/json) IPv6 API
|
|
387
|
+
### Changed
|
|
388
|
+
- [freeipapi.com](https://freeipapi.com/api/json) IPv4 API bug fixed
|
|
389
|
+
- `README.md` updated
|
|
390
|
+
### Removed
|
|
391
|
+
- `IPv4HTTPAdapter` class
|
|
392
|
+
- `_get_json_ipv4_forced` function
|
|
363
393
|
## [0.5] - 2025-10-17
|
|
364
394
|
### Added
|
|
365
395
|
- `setup-warp` action
|
|
@@ -438,7 +468,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
438
468
|
- `--no-geo` argument
|
|
439
469
|
- Logo
|
|
440
470
|
|
|
441
|
-
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.
|
|
471
|
+
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.6...dev
|
|
472
|
+
[0.6]: https://github.com/openscilab/ipspot/compare/v0.5...v0.6
|
|
442
473
|
[0.5]: https://github.com/openscilab/ipspot/compare/v0.4...v0.5
|
|
443
474
|
[0.4]: https://github.com/openscilab/ipspot/compare/v0.3...v0.4
|
|
444
475
|
[0.3]: https://github.com/openscilab/ipspot/compare/v0.2...v0.3
|
|
@@ -32,7 +32,7 @@ def read_description() -> str:
|
|
|
32
32
|
setup(
|
|
33
33
|
name='ipspot',
|
|
34
34
|
packages=['ipspot'],
|
|
35
|
-
version='0.
|
|
35
|
+
version='0.6',
|
|
36
36
|
description='IPSpot: A Python Tool to Fetch the System\'s IP Address',
|
|
37
37
|
long_description=read_description(),
|
|
38
38
|
long_description_content_type='text/markdown',
|
|
@@ -40,7 +40,7 @@ setup(
|
|
|
40
40
|
author='IPSpot Development Team',
|
|
41
41
|
author_email='ipspot@openscilab.com',
|
|
42
42
|
url='https://github.com/openscilab/ipspot',
|
|
43
|
-
download_url='https://github.com/openscilab/ipspot/tarball/v0.
|
|
43
|
+
download_url='https://github.com/openscilab/ipspot/tarball/v0.6',
|
|
44
44
|
keywords="ip ipv4 geo geolocation network location ipspot cli",
|
|
45
45
|
project_urls={
|
|
46
46
|
'Source': 'https://github.com/openscilab/ipspot'
|
|
@@ -48,7 +48,7 @@ setup(
|
|
|
48
48
|
install_requires=get_requires(),
|
|
49
49
|
python_requires='>=3.7',
|
|
50
50
|
classifiers=[
|
|
51
|
-
'Development Status ::
|
|
51
|
+
'Development Status :: 4 - Beta',
|
|
52
52
|
'Natural Language :: English',
|
|
53
53
|
'License :: OSI Approved :: MIT License',
|
|
54
54
|
'Operating System :: OS Independent',
|
|
@@ -123,7 +123,7 @@ def test_public_ipv4_tnedi_me_success():
|
|
|
123
123
|
assert result["data"]["api"] == "tnedi.me"
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
def
|
|
126
|
+
def test_public_ipv4_reallyfreegeoip_org_success():
|
|
127
127
|
result = get_public_ipv4(api=IPv4API.REALLYFREEGEOIP_ORG, geo=True, timeout=40, max_retries=4, retry_delay=90)
|
|
128
128
|
assert result["status"]
|
|
129
129
|
assert is_ipv4(result["data"]["ip"])
|
|
@@ -44,6 +44,38 @@ def test_public_ipv6_my_ip_io_success():
|
|
|
44
44
|
assert result["data"]["api"] == "my-ip.io"
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def test_public_ipv6_ifconfig_co_success():
|
|
48
|
+
result = get_public_ipv6(api=IPv6API.IFCONFIG_CO, geo=True, timeout=40, max_retries=4, retry_delay=90)
|
|
49
|
+
assert result["status"]
|
|
50
|
+
assert is_ipv6(result["data"]["ip"])
|
|
51
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
52
|
+
assert result["data"]["api"] == "ifconfig.co"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_public_ipv6_reallyfreegeoip_org_success():
|
|
56
|
+
result = get_public_ipv6(api=IPv6API.REALLYFREEGEOIP_ORG, geo=True, timeout=40, max_retries=4, retry_delay=90)
|
|
57
|
+
assert result["status"]
|
|
58
|
+
assert is_ipv6(result["data"]["ip"])
|
|
59
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
60
|
+
assert result["data"]["api"] == "reallyfreegeoip.org"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_public_ipv6_myip_la_success():
|
|
64
|
+
result = get_public_ipv6(api=IPv6API.MYIP_LA, geo=True, timeout=40, max_retries=4, retry_delay=90)
|
|
65
|
+
assert result["status"]
|
|
66
|
+
assert is_ipv6(result["data"]["ip"])
|
|
67
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
68
|
+
assert result["data"]["api"] == "myip.la"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_public_freeipapi_com_success():
|
|
72
|
+
result = get_public_ipv6(api=IPv6API.FREEIPAPI_COM, geo=True, timeout=40, max_retries=4, retry_delay=90)
|
|
73
|
+
assert result["status"]
|
|
74
|
+
assert is_ipv6(result["data"]["ip"])
|
|
75
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
76
|
+
assert result["data"]["api"] == "freeipapi.com"
|
|
77
|
+
|
|
78
|
+
|
|
47
79
|
def test_public_ipv6_auto_success():
|
|
48
80
|
result = get_public_ipv6(api=IPv6API.AUTO, geo=True, timeout=40, max_retries=4, retry_delay=90)
|
|
49
81
|
assert result["status"]
|
ipspot-0.5/ipspot/utils.py
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""ipspot utils."""
|
|
3
|
-
import time
|
|
4
|
-
import ipaddress
|
|
5
|
-
import requests
|
|
6
|
-
from typing import Callable, Dict
|
|
7
|
-
from typing import Union, Tuple, Any
|
|
8
|
-
from .params import REQUEST_HEADERS
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _attempt_with_retries(
|
|
12
|
-
func: Callable,
|
|
13
|
-
max_retries: int,
|
|
14
|
-
retry_delay: float, **kwargs: dict) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
15
|
-
"""
|
|
16
|
-
Attempt a function call with retries and delay.
|
|
17
|
-
|
|
18
|
-
:param func: function to execute
|
|
19
|
-
:param max_retries: number of retries
|
|
20
|
-
:param retry_delay: delay between retries (in seconds)
|
|
21
|
-
:param kwargs: keyword arguments to pass to the function
|
|
22
|
-
"""
|
|
23
|
-
max_retries = max(0, max_retries)
|
|
24
|
-
result = {"status": False, "error": ""}
|
|
25
|
-
for attempt in range(max_retries + 1):
|
|
26
|
-
result = func(**kwargs)
|
|
27
|
-
if result["status"]:
|
|
28
|
-
break
|
|
29
|
-
time.sleep(retry_delay)
|
|
30
|
-
return result
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def is_loopback(ip: str) -> bool:
|
|
34
|
-
"""
|
|
35
|
-
Check if the given input IP is a loopback address.
|
|
36
|
-
|
|
37
|
-
:param ip: input IP
|
|
38
|
-
"""
|
|
39
|
-
try:
|
|
40
|
-
ip_object = ipaddress.ip_address(ip)
|
|
41
|
-
return ip_object.is_loopback
|
|
42
|
-
except Exception:
|
|
43
|
-
return False
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _filter_parameter(parameter: Any) -> Any:
|
|
47
|
-
"""
|
|
48
|
-
Filter input parameter.
|
|
49
|
-
|
|
50
|
-
:param parameter: input parameter
|
|
51
|
-
"""
|
|
52
|
-
if parameter is None:
|
|
53
|
-
return "N/A"
|
|
54
|
-
if isinstance(parameter, str) and len(parameter.strip()) == 0:
|
|
55
|
-
return "N/A"
|
|
56
|
-
return parameter
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _get_json_standard(url: str, timeout: Union[float, Tuple[float, float]]) -> dict:
|
|
60
|
-
"""
|
|
61
|
-
Send standard GET request that returns JSON response.
|
|
62
|
-
|
|
63
|
-
:param url: API url
|
|
64
|
-
:param timeout: timeout value for API
|
|
65
|
-
"""
|
|
66
|
-
with requests.Session() as session:
|
|
67
|
-
response = session.get(url, headers=REQUEST_HEADERS, timeout=timeout)
|
|
68
|
-
response.raise_for_status()
|
|
69
|
-
return response.json()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|