ipspot 0.2__tar.gz → 0.3__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.2 → ipspot-0.3}/CHANGELOG.md +19 -1
- {ipspot-0.2 → ipspot-0.3}/PKG-INFO +30 -12
- {ipspot-0.2 → ipspot-0.3}/README.md +9 -9
- {ipspot-0.2 → ipspot-0.3}/SECURITY.md +2 -2
- {ipspot-0.2 → ipspot-0.3}/ipspot/__init__.py +2 -1
- {ipspot-0.2 → ipspot-0.3}/ipspot/__main__.py +1 -1
- ipspot-0.3/ipspot/cli.py +73 -0
- ipspot-0.3/ipspot/ipv4.py +273 -0
- {ipspot-0.2 → ipspot-0.3}/ipspot/params.py +3 -1
- ipspot-0.3/ipspot/utils.py +30 -0
- {ipspot-0.2 → ipspot-0.3}/ipspot.egg-info/PKG-INFO +30 -12
- {ipspot-0.2 → ipspot-0.3}/ipspot.egg-info/SOURCES.txt +5 -3
- ipspot-0.3/ipspot.egg-info/entry_points.txt +2 -0
- {ipspot-0.2 → ipspot-0.3}/setup.py +3 -3
- ipspot-0.3/tests/test_ipv4.py +202 -0
- ipspot-0.2/tests/test_functions.py → ipspot-0.3/tests/test_utils.py +2 -2
- ipspot-0.2/ipspot/functions.py +0 -218
- ipspot-0.2/ipspot.egg-info/entry_points.txt +0 -2
- ipspot-0.2/tests/test_ipv4.py +0 -108
- {ipspot-0.2 → ipspot-0.3}/AUTHORS.md +0 -0
- {ipspot-0.2 → ipspot-0.3}/LICENSE +0 -0
- {ipspot-0.2 → ipspot-0.3}/MANIFEST.in +0 -0
- {ipspot-0.2 → ipspot-0.3}/dev-requirements.txt +0 -0
- {ipspot-0.2 → ipspot-0.3}/ipspot.egg-info/dependency_links.txt +0 -0
- {ipspot-0.2 → ipspot-0.3}/ipspot.egg-info/requires.txt +0 -0
- {ipspot-0.2 → ipspot-0.3}/ipspot.egg-info/top_level.txt +0 -0
- {ipspot-0.2 → ipspot-0.3}/requirements.txt +0 -0
- {ipspot-0.2 → ipspot-0.3}/setup.cfg +0 -0
|
@@ -5,6 +5,23 @@ 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.3] - 2025-05-19
|
|
9
|
+
### Added
|
|
10
|
+
- `is_ipv4` function
|
|
11
|
+
- `is_loopback` function
|
|
12
|
+
- `IPv4HTTPAdapter` class
|
|
13
|
+
- Support [ident.me](https://ident.me/json)
|
|
14
|
+
- Support [tnedi.me](https://tnedi.me/json)
|
|
15
|
+
### Changed
|
|
16
|
+
- `get_private_ipv4` function modified
|
|
17
|
+
- `get_public_ipv4` function modified
|
|
18
|
+
- `_ipsb_ipv4` function modified
|
|
19
|
+
- `_ipapi_ipv4` function modified
|
|
20
|
+
- `_ipinfo_ipv4` function modified
|
|
21
|
+
- `functions.py` renamed to `utils.py`
|
|
22
|
+
- CLI functions moved to `cli.py`
|
|
23
|
+
- IPv4 functions moved to `ipv4.py`
|
|
24
|
+
- Test system modified
|
|
8
25
|
## [0.2] - 2025-05-04
|
|
9
26
|
### Added
|
|
10
27
|
- Support [ip.sb](https://api.ip.sb/geoip)
|
|
@@ -24,7 +41,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
24
41
|
- `--no-geo` argument
|
|
25
42
|
- Logo
|
|
26
43
|
|
|
27
|
-
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.
|
|
44
|
+
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.3...dev
|
|
45
|
+
[0.3]: https://github.com/openscilab/ipspot/compare/v0.2...v0.3
|
|
28
46
|
[0.2]: https://github.com/openscilab/ipspot/compare/v0.1...v0.2
|
|
29
47
|
[0.1]: https://github.com/openscilab/ipspot/compare/3216fb7...v0.1
|
|
30
48
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipspot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3
|
|
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.3
|
|
7
7
|
Author: IPSpot Development Team
|
|
8
8
|
Author-email: ipspot@openscilab.com
|
|
9
9
|
License: MIT
|
|
@@ -102,13 +102,13 @@ Dynamic: summary
|
|
|
102
102
|
## Installation
|
|
103
103
|
|
|
104
104
|
### Source Code
|
|
105
|
-
- Download [Version 0.
|
|
105
|
+
- Download [Version 0.3](https://github.com/openscilab/ipspot/archive/v0.3.zip) or [Latest Source](https://github.com/openscilab/ipspot/archive/dev.zip)
|
|
106
106
|
- `pip install .`
|
|
107
107
|
|
|
108
108
|
### PyPI
|
|
109
109
|
|
|
110
110
|
- Check [Python Packaging User Guide](https://packaging.python.org/installing/)
|
|
111
|
-
- `pip install ipspot==0.
|
|
111
|
+
- `pip install ipspot==0.3`
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
## Usage
|
|
@@ -142,7 +142,7 @@ Dynamic: summary
|
|
|
142
142
|
```console
|
|
143
143
|
> ipspot --version
|
|
144
144
|
|
|
145
|
-
0.
|
|
145
|
+
0.3
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
#### Info
|
|
@@ -157,11 +157,11 @@ Dynamic: summary
|
|
|
157
157
|
|___||_| |____/ | .__/ \___/ \__|
|
|
158
158
|
|_|
|
|
159
159
|
|
|
160
|
-
__ __ ___
|
|
161
|
-
\ \ / / _ / _ \ |___
|
|
162
|
-
\ \ / / (_)| | | |
|
|
163
|
-
\ V / _ | |_| | _
|
|
164
|
-
\_/ (_) \___/ (_)|
|
|
160
|
+
__ __ ___ _____
|
|
161
|
+
\ \ / / _ / _ \ |___ /
|
|
162
|
+
\ \ / / (_)| | | | |_ \
|
|
163
|
+
\ V / _ | |_| | _ ___) |
|
|
164
|
+
\_/ (_) \___/ (_)|____/
|
|
165
165
|
|
|
166
166
|
|
|
167
167
|
|
|
@@ -197,7 +197,7 @@ Public IP and Location Info:
|
|
|
197
197
|
|
|
198
198
|
#### IPv4 API
|
|
199
199
|
|
|
200
|
-
ℹ️ `ipv4-api` valid choices: [`auto`, `ipapi`, `ipinfo`, `ipsb`]
|
|
200
|
+
ℹ️ `ipv4-api` valid choices: [`auto`, `ipapi`, `ipinfo`, `ipsb`, `identme`, `tnedime`]
|
|
201
201
|
|
|
202
202
|
ℹ️ The default value: `auto`
|
|
203
203
|
|
|
@@ -267,6 +267,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
|
267
267
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
268
268
|
|
|
269
269
|
## [Unreleased]
|
|
270
|
+
## [0.3] - 2025-05-19
|
|
271
|
+
### Added
|
|
272
|
+
- `is_ipv4` function
|
|
273
|
+
- `is_loopback` function
|
|
274
|
+
- `IPv4HTTPAdapter` class
|
|
275
|
+
- Support [ident.me](https://ident.me/json)
|
|
276
|
+
- Support [tnedi.me](https://tnedi.me/json)
|
|
277
|
+
### Changed
|
|
278
|
+
- `get_private_ipv4` function modified
|
|
279
|
+
- `get_public_ipv4` function modified
|
|
280
|
+
- `_ipsb_ipv4` function modified
|
|
281
|
+
- `_ipapi_ipv4` function modified
|
|
282
|
+
- `_ipinfo_ipv4` function modified
|
|
283
|
+
- `functions.py` renamed to `utils.py`
|
|
284
|
+
- CLI functions moved to `cli.py`
|
|
285
|
+
- IPv4 functions moved to `ipv4.py`
|
|
286
|
+
- Test system modified
|
|
270
287
|
## [0.2] - 2025-05-04
|
|
271
288
|
### Added
|
|
272
289
|
- Support [ip.sb](https://api.ip.sb/geoip)
|
|
@@ -286,7 +303,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
286
303
|
- `--no-geo` argument
|
|
287
304
|
- Logo
|
|
288
305
|
|
|
289
|
-
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.
|
|
306
|
+
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.3...dev
|
|
307
|
+
[0.3]: https://github.com/openscilab/ipspot/compare/v0.2...v0.3
|
|
290
308
|
[0.2]: https://github.com/openscilab/ipspot/compare/v0.1...v0.2
|
|
291
309
|
[0.1]: https://github.com/openscilab/ipspot/compare/3216fb7...v0.1
|
|
292
310
|
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
## Installation
|
|
53
53
|
|
|
54
54
|
### Source Code
|
|
55
|
-
- Download [Version 0.
|
|
55
|
+
- Download [Version 0.3](https://github.com/openscilab/ipspot/archive/v0.3.zip) or [Latest Source](https://github.com/openscilab/ipspot/archive/dev.zip)
|
|
56
56
|
- `pip install .`
|
|
57
57
|
|
|
58
58
|
### PyPI
|
|
59
59
|
|
|
60
60
|
- Check [Python Packaging User Guide](https://packaging.python.org/installing/)
|
|
61
|
-
- `pip install ipspot==0.
|
|
61
|
+
- `pip install ipspot==0.3`
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
## Usage
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
```console
|
|
93
93
|
> ipspot --version
|
|
94
94
|
|
|
95
|
-
0.
|
|
95
|
+
0.3
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
#### Info
|
|
@@ -107,11 +107,11 @@
|
|
|
107
107
|
|___||_| |____/ | .__/ \___/ \__|
|
|
108
108
|
|_|
|
|
109
109
|
|
|
110
|
-
__ __ ___
|
|
111
|
-
\ \ / / _ / _ \ |___
|
|
112
|
-
\ \ / / (_)| | | |
|
|
113
|
-
\ V / _ | |_| | _
|
|
114
|
-
\_/ (_) \___/ (_)|
|
|
110
|
+
__ __ ___ _____
|
|
111
|
+
\ \ / / _ / _ \ |___ /
|
|
112
|
+
\ \ / / (_)| | | | |_ \
|
|
113
|
+
\ V / _ | |_| | _ ___) |
|
|
114
|
+
\_/ (_) \___/ (_)|____/
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
|
|
@@ -147,7 +147,7 @@ Public IP and Location Info:
|
|
|
147
147
|
|
|
148
148
|
#### IPv4 API
|
|
149
149
|
|
|
150
|
-
ℹ️ `ipv4-api` valid choices: [`auto`, `ipapi`, `ipinfo`, `ipsb`]
|
|
150
|
+
ℹ️ `ipv4-api` valid choices: [`auto`, `ipapi`, `ipinfo`, `ipsb`, `identme`, `tnedime`]
|
|
151
151
|
|
|
152
152
|
ℹ️ The default value: `auto`
|
|
153
153
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""ipspot modules."""
|
|
3
3
|
from .params import IPSPOT_VERSION, IPv4API
|
|
4
|
-
from .
|
|
4
|
+
from .ipv4 import get_private_ipv4, get_public_ipv4, is_ipv4
|
|
5
|
+
from .utils import is_loopback
|
|
5
6
|
__version__ = IPSPOT_VERSION
|
ipspot-0.3/ipspot/cli.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ipspot CLI."""
|
|
3
|
+
import argparse
|
|
4
|
+
from typing import Union, Tuple
|
|
5
|
+
from art import tprint
|
|
6
|
+
from .ipv4 import get_public_ipv4, get_private_ipv4
|
|
7
|
+
from .utils import filter_parameter
|
|
8
|
+
from .params import IPv4API, PARAMETERS_NAME_MAP
|
|
9
|
+
from .params import IPSPOT_OVERVIEW, IPSPOT_REPO, IPSPOT_VERSION
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ipspot_info() -> None: # pragma: no cover
|
|
13
|
+
"""Print ipspot details."""
|
|
14
|
+
tprint("IPSpot")
|
|
15
|
+
tprint("V:" + IPSPOT_VERSION)
|
|
16
|
+
print(IPSPOT_OVERVIEW)
|
|
17
|
+
print("Repo : " + IPSPOT_REPO)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def display_ip_info(ipv4_api: IPv4API = IPv4API.AUTO, geo: bool=False,
|
|
21
|
+
timeout: Union[float, Tuple[float, float]]=5) -> None: # pragma: no cover
|
|
22
|
+
"""
|
|
23
|
+
Print collected IP and location data.
|
|
24
|
+
|
|
25
|
+
:param ipv4_api: public IPv4 API
|
|
26
|
+
:param geo: geolocation flag
|
|
27
|
+
:param timeout: timeout value for API
|
|
28
|
+
"""
|
|
29
|
+
private_result = get_private_ipv4()
|
|
30
|
+
print("Private IP:\n")
|
|
31
|
+
print(" IP: {private_result[data][ip]}".format(private_result=private_result) if private_result["status"]
|
|
32
|
+
else " Error: {private_result[error]}".format(private_result=private_result))
|
|
33
|
+
|
|
34
|
+
public_title = "\nPublic IP"
|
|
35
|
+
if geo:
|
|
36
|
+
public_title += " and Location Info"
|
|
37
|
+
public_title += ":\n"
|
|
38
|
+
print(public_title)
|
|
39
|
+
public_result = get_public_ipv4(ipv4_api, geo=geo, timeout=timeout)
|
|
40
|
+
if public_result["status"]:
|
|
41
|
+
for name, parameter in sorted(public_result["data"].items()):
|
|
42
|
+
print(
|
|
43
|
+
" {name}: {parameter}".format(
|
|
44
|
+
name=PARAMETERS_NAME_MAP[name],
|
|
45
|
+
parameter=filter_parameter(parameter)))
|
|
46
|
+
else:
|
|
47
|
+
print(" Error: {public_result[error]}".format(public_result=public_result))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None: # pragma: no cover
|
|
51
|
+
"""CLI main function."""
|
|
52
|
+
parser = argparse.ArgumentParser()
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
'--ipv4-api',
|
|
55
|
+
help='public IPv4 API',
|
|
56
|
+
type=str.lower,
|
|
57
|
+
choices=[
|
|
58
|
+
x.value for x in IPv4API],
|
|
59
|
+
default=IPv4API.AUTO.value)
|
|
60
|
+
parser.add_argument('--info', help='info', nargs="?", const=1)
|
|
61
|
+
parser.add_argument('--version', help='version', nargs="?", const=1)
|
|
62
|
+
parser.add_argument('--no-geo', help='no geolocation data', nargs="?", const=1, default=False)
|
|
63
|
+
parser.add_argument('--timeout', help='timeout for the API request', type=float, default=5.0)
|
|
64
|
+
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
if args.version:
|
|
67
|
+
print(IPSPOT_VERSION)
|
|
68
|
+
elif args.info:
|
|
69
|
+
ipspot_info()
|
|
70
|
+
else:
|
|
71
|
+
ipv4_api = IPv4API(args.ipv4_api)
|
|
72
|
+
geo = not args.no_geo
|
|
73
|
+
display_ip_info(ipv4_api=ipv4_api, geo=geo, timeout=args.timeout)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ipspot ipv4 functions."""
|
|
3
|
+
import ipaddress
|
|
4
|
+
import socket
|
|
5
|
+
from typing import Union, Dict, List, Tuple
|
|
6
|
+
import requests
|
|
7
|
+
from requests.adapters import HTTPAdapter
|
|
8
|
+
from urllib3.poolmanager import PoolManager
|
|
9
|
+
from .utils import is_loopback
|
|
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 is_ipv4(ip: str) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check if the given input is a valid IPv4 address.
|
|
59
|
+
|
|
60
|
+
:param ip: input IP
|
|
61
|
+
"""
|
|
62
|
+
if not isinstance(ip, str):
|
|
63
|
+
return False
|
|
64
|
+
try:
|
|
65
|
+
_ = ipaddress.IPv4Address(ip)
|
|
66
|
+
return True
|
|
67
|
+
except Exception:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_private_ipv4() -> Dict[str, Union[bool, Dict[str, str], str]]:
|
|
72
|
+
"""Retrieve the private IPv4 address."""
|
|
73
|
+
try:
|
|
74
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
|
75
|
+
s.connect(('192.168.1.1', 1))
|
|
76
|
+
private_ip = s.getsockname()[0]
|
|
77
|
+
if is_ipv4(private_ip) and not is_loopback(private_ip):
|
|
78
|
+
return {"status": True, "data": {"ip": private_ip}}
|
|
79
|
+
return {"status": False, "error": "Could not identify a non-loopback IPv4 address for this system."}
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return {"status": False, "error": str(e)}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _ipsb_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
85
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
86
|
+
"""
|
|
87
|
+
Get public IP and geolocation using ip.sb.
|
|
88
|
+
|
|
89
|
+
:param geo: geolocation flag
|
|
90
|
+
:param timeout: timeout value for API
|
|
91
|
+
"""
|
|
92
|
+
try:
|
|
93
|
+
with requests.Session() as session:
|
|
94
|
+
response = session.get("https://api-ipv4.ip.sb/geoip", headers=REQUEST_HEADERS, timeout=timeout)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
data = response.json()
|
|
97
|
+
result = {"status": True, "data": {"ip": data.get("ip"), "api": "ip.sb"}}
|
|
98
|
+
if geo:
|
|
99
|
+
geo_data = {
|
|
100
|
+
"city": data.get("city"),
|
|
101
|
+
"region": data.get("region"),
|
|
102
|
+
"country": data.get("country"),
|
|
103
|
+
"country_code": data.get("country_code"),
|
|
104
|
+
"latitude": data.get("latitude"),
|
|
105
|
+
"longitude": data.get("longitude"),
|
|
106
|
+
"organization": data.get("organization"),
|
|
107
|
+
"timezone": data.get("timezone")
|
|
108
|
+
}
|
|
109
|
+
result["data"].update(geo_data)
|
|
110
|
+
return result
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return {"status": False, "error": str(e)}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _ipapi_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
116
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
117
|
+
"""
|
|
118
|
+
Get public IP and geolocation using ip-api.com.
|
|
119
|
+
|
|
120
|
+
:param geo: geolocation flag
|
|
121
|
+
:param timeout: timeout value for API
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
with requests.Session() as session:
|
|
125
|
+
session.mount("http://", IPv4HTTPAdapter())
|
|
126
|
+
session.mount("https://", IPv4HTTPAdapter())
|
|
127
|
+
response = session.get("http://ip-api.com/json/", headers=REQUEST_HEADERS, timeout=timeout)
|
|
128
|
+
response.raise_for_status()
|
|
129
|
+
data = response.json()
|
|
130
|
+
if data.get("status") != "success":
|
|
131
|
+
return {"status": False, "error": "ip-api lookup failed"}
|
|
132
|
+
result = {"status": True, "data": {"ip": data.get("query"), "api": "ip-api.com"}}
|
|
133
|
+
if geo:
|
|
134
|
+
geo_data = {
|
|
135
|
+
"city": data.get("city"),
|
|
136
|
+
"region": data.get("regionName"),
|
|
137
|
+
"country": data.get("country"),
|
|
138
|
+
"country_code": data.get("countryCode"),
|
|
139
|
+
"latitude": data.get("lat"),
|
|
140
|
+
"longitude": data.get("lon"),
|
|
141
|
+
"organization": data.get("org"),
|
|
142
|
+
"timezone": data.get("timezone")
|
|
143
|
+
}
|
|
144
|
+
result["data"].update(geo_data)
|
|
145
|
+
return result
|
|
146
|
+
except Exception as e:
|
|
147
|
+
return {"status": False, "error": str(e)}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _ipinfo_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
151
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
152
|
+
"""
|
|
153
|
+
Get public IP and geolocation using ipinfo.io.
|
|
154
|
+
|
|
155
|
+
:param geo: geolocation flag
|
|
156
|
+
:param timeout: timeout value for API
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
with requests.Session() as session:
|
|
160
|
+
session.mount("http://", IPv4HTTPAdapter())
|
|
161
|
+
session.mount("https://", IPv4HTTPAdapter())
|
|
162
|
+
response = session.get("https://ipinfo.io/json", headers=REQUEST_HEADERS, timeout=timeout)
|
|
163
|
+
response.raise_for_status()
|
|
164
|
+
data = response.json()
|
|
165
|
+
result = {"status": True, "data": {"ip": data.get("ip"), "api": "ipinfo.io"}}
|
|
166
|
+
if geo:
|
|
167
|
+
loc = data.get("loc", "").split(",")
|
|
168
|
+
geo_data = {
|
|
169
|
+
"city": data.get("city"),
|
|
170
|
+
"region": data.get("region"),
|
|
171
|
+
"country": None,
|
|
172
|
+
"country_code": data.get("country"),
|
|
173
|
+
"latitude": float(loc[0]) if len(loc) == 2 else None,
|
|
174
|
+
"longitude": float(loc[1]) if len(loc) == 2 else None,
|
|
175
|
+
"organization": data.get("org"),
|
|
176
|
+
"timezone": data.get("timezone")
|
|
177
|
+
}
|
|
178
|
+
result["data"].update(geo_data)
|
|
179
|
+
return result
|
|
180
|
+
except Exception as e:
|
|
181
|
+
return {"status": False, "error": str(e)}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _ident_me_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
185
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
186
|
+
"""
|
|
187
|
+
Get public IP and geolocation using ident.me.
|
|
188
|
+
|
|
189
|
+
:param geo: geolocation flag
|
|
190
|
+
:param timeout: timeout value for API
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
with requests.Session() as session:
|
|
194
|
+
response = session.get("https://4.ident.me/json", headers=REQUEST_HEADERS, timeout=timeout)
|
|
195
|
+
response.raise_for_status()
|
|
196
|
+
data = response.json()
|
|
197
|
+
result = {"status": True, "data": {"ip": data.get("ip"), "api": "ident.me"}}
|
|
198
|
+
if geo:
|
|
199
|
+
geo_data = {
|
|
200
|
+
"city": data.get("city"),
|
|
201
|
+
"region": None,
|
|
202
|
+
"country": data.get("country"),
|
|
203
|
+
"country_code": data.get("cc"),
|
|
204
|
+
"latitude": data.get("latitude"),
|
|
205
|
+
"longitude": data.get("longitude"),
|
|
206
|
+
"organization": data.get("aso"),
|
|
207
|
+
"timezone": data.get("tz")
|
|
208
|
+
}
|
|
209
|
+
result["data"].update(geo_data)
|
|
210
|
+
return result
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return {"status": False, "error": str(e)}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _tnedime_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
216
|
+
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
217
|
+
"""
|
|
218
|
+
Get public IP and geolocation using tnedi.me.
|
|
219
|
+
|
|
220
|
+
:param geo: geolocation flag
|
|
221
|
+
:param timeout: timeout value for API
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
with requests.Session() as session:
|
|
225
|
+
response = session.get("https://4.tnedi.me/json", headers=REQUEST_HEADERS, timeout=timeout)
|
|
226
|
+
response.raise_for_status()
|
|
227
|
+
data = response.json()
|
|
228
|
+
result = {"status": True, "data": {"ip": data.get("ip"), "api": "tnedi.me"}}
|
|
229
|
+
if geo:
|
|
230
|
+
geo_data = {
|
|
231
|
+
"city": data.get("city"),
|
|
232
|
+
"region": None,
|
|
233
|
+
"country": data.get("country"),
|
|
234
|
+
"country_code": data.get("cc"),
|
|
235
|
+
"latitude": data.get("latitude"),
|
|
236
|
+
"longitude": data.get("longitude"),
|
|
237
|
+
"organization": data.get("aso"),
|
|
238
|
+
"timezone": data.get("tz")
|
|
239
|
+
}
|
|
240
|
+
result["data"].update(geo_data)
|
|
241
|
+
return result
|
|
242
|
+
except Exception as e:
|
|
243
|
+
return {"status": False, "error": str(e)}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def get_public_ipv4(api: IPv4API=IPv4API.AUTO, geo: bool=False,
|
|
247
|
+
timeout: Union[float, Tuple[float, float]]=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
248
|
+
"""
|
|
249
|
+
Get public IPv4 and geolocation info based on the selected API.
|
|
250
|
+
|
|
251
|
+
:param api: public IPv4 API
|
|
252
|
+
:param geo: geolocation flag
|
|
253
|
+
:param timeout: timeout value for API
|
|
254
|
+
"""
|
|
255
|
+
api_map = {
|
|
256
|
+
IPv4API.IDENTME: _ident_me_ipv4,
|
|
257
|
+
IPv4API.TNEDIME: _tnedime_ipv4,
|
|
258
|
+
IPv4API.IPSB: _ipsb_ipv4,
|
|
259
|
+
IPv4API.IPAPI: _ipapi_ipv4,
|
|
260
|
+
IPv4API.IPINFO: _ipinfo_ipv4,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if api == IPv4API.AUTO:
|
|
264
|
+
for _, func in api_map.items():
|
|
265
|
+
result = func(geo=geo, timeout=timeout)
|
|
266
|
+
if result["status"]:
|
|
267
|
+
return result
|
|
268
|
+
return {"status": False, "error": "All attempts failed."}
|
|
269
|
+
else:
|
|
270
|
+
func = api_map.get(api)
|
|
271
|
+
if func:
|
|
272
|
+
return func(geo=geo, timeout=timeout)
|
|
273
|
+
return {"status": False, "error": "Unsupported API: {api}".format(api=api)}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""ipspot params."""
|
|
3
3
|
from enum import Enum
|
|
4
4
|
|
|
5
|
-
IPSPOT_VERSION = "0.
|
|
5
|
+
IPSPOT_VERSION = "0.3"
|
|
6
6
|
|
|
7
7
|
IPSPOT_OVERVIEW = '''
|
|
8
8
|
IPSpot is a Python library for retrieving the current system's IP address and location information.
|
|
@@ -25,6 +25,8 @@ class IPv4API(Enum):
|
|
|
25
25
|
IPAPI = "ipapi"
|
|
26
26
|
IPINFO = "ipinfo"
|
|
27
27
|
IPSB = "ipsb"
|
|
28
|
+
IDENTME = "identme"
|
|
29
|
+
TNEDIME = "tnedime"
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
PARAMETERS_NAME_MAP = {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""ipspot utils."""
|
|
3
|
+
import ipaddress
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_loopback(ip: str) -> bool:
|
|
8
|
+
"""
|
|
9
|
+
Check if the given input IP is a loopback address.
|
|
10
|
+
|
|
11
|
+
:param ip: input IP
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
ip_object = ipaddress.ip_address(ip)
|
|
15
|
+
return ip_object.is_loopback
|
|
16
|
+
except Exception:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def filter_parameter(parameter: Any) -> Any:
|
|
21
|
+
"""
|
|
22
|
+
Filter input parameter.
|
|
23
|
+
|
|
24
|
+
:param parameter: input parameter
|
|
25
|
+
"""
|
|
26
|
+
if parameter is None:
|
|
27
|
+
return "N/A"
|
|
28
|
+
if isinstance(parameter, str) and len(parameter.strip()) == 0:
|
|
29
|
+
return "N/A"
|
|
30
|
+
return parameter
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipspot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3
|
|
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.3
|
|
7
7
|
Author: IPSpot Development Team
|
|
8
8
|
Author-email: ipspot@openscilab.com
|
|
9
9
|
License: MIT
|
|
@@ -102,13 +102,13 @@ Dynamic: summary
|
|
|
102
102
|
## Installation
|
|
103
103
|
|
|
104
104
|
### Source Code
|
|
105
|
-
- Download [Version 0.
|
|
105
|
+
- Download [Version 0.3](https://github.com/openscilab/ipspot/archive/v0.3.zip) or [Latest Source](https://github.com/openscilab/ipspot/archive/dev.zip)
|
|
106
106
|
- `pip install .`
|
|
107
107
|
|
|
108
108
|
### PyPI
|
|
109
109
|
|
|
110
110
|
- Check [Python Packaging User Guide](https://packaging.python.org/installing/)
|
|
111
|
-
- `pip install ipspot==0.
|
|
111
|
+
- `pip install ipspot==0.3`
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
## Usage
|
|
@@ -142,7 +142,7 @@ Dynamic: summary
|
|
|
142
142
|
```console
|
|
143
143
|
> ipspot --version
|
|
144
144
|
|
|
145
|
-
0.
|
|
145
|
+
0.3
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
#### Info
|
|
@@ -157,11 +157,11 @@ Dynamic: summary
|
|
|
157
157
|
|___||_| |____/ | .__/ \___/ \__|
|
|
158
158
|
|_|
|
|
159
159
|
|
|
160
|
-
__ __ ___
|
|
161
|
-
\ \ / / _ / _ \ |___
|
|
162
|
-
\ \ / / (_)| | | |
|
|
163
|
-
\ V / _ | |_| | _
|
|
164
|
-
\_/ (_) \___/ (_)|
|
|
160
|
+
__ __ ___ _____
|
|
161
|
+
\ \ / / _ / _ \ |___ /
|
|
162
|
+
\ \ / / (_)| | | | |_ \
|
|
163
|
+
\ V / _ | |_| | _ ___) |
|
|
164
|
+
\_/ (_) \___/ (_)|____/
|
|
165
165
|
|
|
166
166
|
|
|
167
167
|
|
|
@@ -197,7 +197,7 @@ Public IP and Location Info:
|
|
|
197
197
|
|
|
198
198
|
#### IPv4 API
|
|
199
199
|
|
|
200
|
-
ℹ️ `ipv4-api` valid choices: [`auto`, `ipapi`, `ipinfo`, `ipsb`]
|
|
200
|
+
ℹ️ `ipv4-api` valid choices: [`auto`, `ipapi`, `ipinfo`, `ipsb`, `identme`, `tnedime`]
|
|
201
201
|
|
|
202
202
|
ℹ️ The default value: `auto`
|
|
203
203
|
|
|
@@ -267,6 +267,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
|
267
267
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
268
268
|
|
|
269
269
|
## [Unreleased]
|
|
270
|
+
## [0.3] - 2025-05-19
|
|
271
|
+
### Added
|
|
272
|
+
- `is_ipv4` function
|
|
273
|
+
- `is_loopback` function
|
|
274
|
+
- `IPv4HTTPAdapter` class
|
|
275
|
+
- Support [ident.me](https://ident.me/json)
|
|
276
|
+
- Support [tnedi.me](https://tnedi.me/json)
|
|
277
|
+
### Changed
|
|
278
|
+
- `get_private_ipv4` function modified
|
|
279
|
+
- `get_public_ipv4` function modified
|
|
280
|
+
- `_ipsb_ipv4` function modified
|
|
281
|
+
- `_ipapi_ipv4` function modified
|
|
282
|
+
- `_ipinfo_ipv4` function modified
|
|
283
|
+
- `functions.py` renamed to `utils.py`
|
|
284
|
+
- CLI functions moved to `cli.py`
|
|
285
|
+
- IPv4 functions moved to `ipv4.py`
|
|
286
|
+
- Test system modified
|
|
270
287
|
## [0.2] - 2025-05-04
|
|
271
288
|
### Added
|
|
272
289
|
- Support [ip.sb](https://api.ip.sb/geoip)
|
|
@@ -286,7 +303,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
286
303
|
- `--no-geo` argument
|
|
287
304
|
- Logo
|
|
288
305
|
|
|
289
|
-
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.
|
|
306
|
+
[Unreleased]: https://github.com/openscilab/ipspot/compare/v0.3...dev
|
|
307
|
+
[0.3]: https://github.com/openscilab/ipspot/compare/v0.2...v0.3
|
|
290
308
|
[0.2]: https://github.com/openscilab/ipspot/compare/v0.1...v0.2
|
|
291
309
|
[0.1]: https://github.com/openscilab/ipspot/compare/3216fb7...v0.1
|
|
292
310
|
|
|
@@ -9,13 +9,15 @@ requirements.txt
|
|
|
9
9
|
setup.py
|
|
10
10
|
ipspot/__init__.py
|
|
11
11
|
ipspot/__main__.py
|
|
12
|
-
ipspot/
|
|
12
|
+
ipspot/cli.py
|
|
13
|
+
ipspot/ipv4.py
|
|
13
14
|
ipspot/params.py
|
|
15
|
+
ipspot/utils.py
|
|
14
16
|
ipspot.egg-info/PKG-INFO
|
|
15
17
|
ipspot.egg-info/SOURCES.txt
|
|
16
18
|
ipspot.egg-info/dependency_links.txt
|
|
17
19
|
ipspot.egg-info/entry_points.txt
|
|
18
20
|
ipspot.egg-info/requires.txt
|
|
19
21
|
ipspot.egg-info/top_level.txt
|
|
20
|
-
tests/
|
|
21
|
-
tests/
|
|
22
|
+
tests/test_ipv4.py
|
|
23
|
+
tests/test_utils.py
|
|
@@ -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.3',
|
|
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.3',
|
|
44
44
|
keywords="ip ipv4 geo geolocation network location ipspot cli",
|
|
45
45
|
project_urls={
|
|
46
46
|
'Source': 'https://github.com/openscilab/ipspot'
|
|
@@ -69,7 +69,7 @@ setup(
|
|
|
69
69
|
license='MIT',
|
|
70
70
|
entry_points={
|
|
71
71
|
'console_scripts': [
|
|
72
|
-
'ipspot = ipspot.
|
|
72
|
+
'ipspot = ipspot.cli:main',
|
|
73
73
|
]
|
|
74
74
|
}
|
|
75
75
|
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
import requests
|
|
3
|
+
from ipspot import get_private_ipv4, is_ipv4
|
|
4
|
+
from ipspot import get_public_ipv4, IPv4API
|
|
5
|
+
from ipspot import is_loopback
|
|
6
|
+
|
|
7
|
+
TEST_CASE_NAME = "IPv4 tests"
|
|
8
|
+
DATA_ITEMS = {'country_code', 'latitude', 'longitude', 'api', 'country', 'timezone', 'organization', 'region', 'ip', 'city'}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_is_ipv4_1():
|
|
12
|
+
assert is_ipv4("192.168.0.1")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_is_ipv4_2():
|
|
16
|
+
assert is_ipv4("0.0.0.0")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_is_ipv4_3():
|
|
20
|
+
assert is_ipv4("255.255.255.255")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_is_ipv4_4():
|
|
24
|
+
assert not is_ipv4("256.0.0.1")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_is_ipv4_5():
|
|
28
|
+
assert not is_ipv4("abc.def.ghi.jkl")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_is_ipv4_6():
|
|
32
|
+
assert not is_ipv4(123)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_is_ipv4_7():
|
|
36
|
+
assert not is_ipv4("2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_is_loopback_1():
|
|
40
|
+
assert not is_loopback("192.168.0.1")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_is_loopback_2():
|
|
44
|
+
assert is_loopback("127.0.0.1")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_is_loopback_3():
|
|
48
|
+
assert is_loopback("127.255.255.255")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_is_loopback_4():
|
|
52
|
+
assert not is_loopback("abc.def.ghi.jkl")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_private_ipv4_success():
|
|
56
|
+
result = get_private_ipv4()
|
|
57
|
+
assert result["status"]
|
|
58
|
+
assert is_ipv4(result["data"]["ip"])
|
|
59
|
+
assert not is_loopback(result["data"]["ip"])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_get_private_ipv4_loopback():
|
|
63
|
+
mock_socket = mock.MagicMock()
|
|
64
|
+
mock_socket.__enter__.return_value.getsockname.return_value = ('127.0.0.1',)
|
|
65
|
+
with mock.patch('socket.socket', return_value=mock_socket):
|
|
66
|
+
result = get_private_ipv4()
|
|
67
|
+
assert not result["status"]
|
|
68
|
+
assert result["error"] == "Could not identify a non-loopback IPv4 address for this system."
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_get_private_ipv4_exception():
|
|
72
|
+
with mock.patch('socket.socket', side_effect=Exception("Test error")):
|
|
73
|
+
result = get_private_ipv4()
|
|
74
|
+
assert not result["status"]
|
|
75
|
+
assert result["error"] == "Test error"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_public_ipv4_auto_success():
|
|
79
|
+
result = get_public_ipv4(api=IPv4API.AUTO, geo=True)
|
|
80
|
+
assert result["status"]
|
|
81
|
+
assert is_ipv4(result["data"]["ip"])
|
|
82
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_public_ipv4_auto_timeout_error():
|
|
86
|
+
result = get_public_ipv4(api=IPv4API.AUTO, geo=True, timeout="5")
|
|
87
|
+
assert not result["status"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_public_ipv4_auto_net_error():
|
|
91
|
+
with mock.patch.object(requests.Session, "get", side_effect=Exception("No Internet")):
|
|
92
|
+
result = get_public_ipv4(api=IPv4API.AUTO)
|
|
93
|
+
assert not result["status"]
|
|
94
|
+
assert result["error"] == "All attempts failed."
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_public_ipv4_ipapi_success():
|
|
98
|
+
result = get_public_ipv4(api=IPv4API.IPAPI, geo=True)
|
|
99
|
+
assert result["status"]
|
|
100
|
+
assert is_ipv4(result["data"]["ip"])
|
|
101
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
102
|
+
assert result["data"]["api"] == "ip-api.com"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_public_ipv4_ipapi_timeout_error():
|
|
106
|
+
result = get_public_ipv4(api=IPv4API.IPAPI, geo=True, timeout="5")
|
|
107
|
+
assert not result["status"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_public_ipv4_ipapi_net_error():
|
|
111
|
+
with mock.patch.object(requests.Session, "get", side_effect=Exception("No Internet")):
|
|
112
|
+
result = get_public_ipv4(api=IPv4API.IPAPI)
|
|
113
|
+
assert not result["status"]
|
|
114
|
+
assert result["error"] == "No Internet"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_public_ipv4_ipinfo_success():
|
|
118
|
+
result = get_public_ipv4(api=IPv4API.IPINFO, geo=True)
|
|
119
|
+
assert result["status"]
|
|
120
|
+
assert is_ipv4(result["data"]["ip"])
|
|
121
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
122
|
+
assert result["data"]["api"] == "ipinfo.io"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_public_ipv4_ipinfo_timeout_error():
|
|
126
|
+
result = get_public_ipv4(api=IPv4API.IPINFO, geo=True, timeout="5")
|
|
127
|
+
assert not result["status"]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_public_ipv4_ipinfo_net_error():
|
|
131
|
+
with mock.patch.object(requests.Session, "get", side_effect=Exception("No Internet")):
|
|
132
|
+
result = get_public_ipv4(api=IPv4API.IPINFO)
|
|
133
|
+
assert not result["status"]
|
|
134
|
+
assert result["error"] == "No Internet"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_public_ipv4_ipsb_success():
|
|
138
|
+
result = get_public_ipv4(api=IPv4API.IPSB, geo=True, timeout=30)
|
|
139
|
+
assert result["status"]
|
|
140
|
+
assert is_ipv4(result["data"]["ip"])
|
|
141
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
142
|
+
assert result["data"]["api"] == "ip.sb"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_public_ipv4_ipsb_timeout_error():
|
|
146
|
+
result = get_public_ipv4(api=IPv4API.IPSB, geo=True, timeout="5")
|
|
147
|
+
assert not result["status"]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_public_ipv4_ipsb_net_error():
|
|
152
|
+
with mock.patch.object(requests.Session, "get", side_effect=Exception("No Internet")):
|
|
153
|
+
result = get_public_ipv4(api=IPv4API.IPSB)
|
|
154
|
+
assert not result["status"]
|
|
155
|
+
assert result["error"] == "No Internet"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_public_ipv4_identme_success():
|
|
159
|
+
result = get_public_ipv4(api=IPv4API.IDENTME, geo=True)
|
|
160
|
+
assert result["status"]
|
|
161
|
+
assert is_ipv4(result["data"]["ip"])
|
|
162
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
163
|
+
assert result["data"]["api"] == "ident.me"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_public_ipv4_identme_timeout_error():
|
|
167
|
+
result = get_public_ipv4(api=IPv4API.IDENTME, geo=True, timeout="5")
|
|
168
|
+
assert not result["status"]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_public_ipv4_identme_net_error():
|
|
172
|
+
with mock.patch.object(requests.Session, "get", side_effect=Exception("No Internet")):
|
|
173
|
+
result = get_public_ipv4(api=IPv4API.IDENTME)
|
|
174
|
+
assert not result["status"]
|
|
175
|
+
assert result["error"] == "No Internet"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_public_ipv4_tnedime_success():
|
|
179
|
+
result = get_public_ipv4(api=IPv4API.TNEDIME, geo=True)
|
|
180
|
+
assert result["status"]
|
|
181
|
+
assert is_ipv4(result["data"]["ip"])
|
|
182
|
+
assert set(result["data"].keys()) == DATA_ITEMS
|
|
183
|
+
assert result["data"]["api"] == "tnedi.me"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_public_ipv4_tnedime_timeout_error():
|
|
187
|
+
result = get_public_ipv4(api=IPv4API.TNEDIME, geo=True, timeout="5")
|
|
188
|
+
assert not result["status"]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_public_ipv4_tnedime_net_error():
|
|
192
|
+
with mock.patch.object(requests.Session, "get", side_effect=Exception("No Internet")):
|
|
193
|
+
result = get_public_ipv4(api=IPv4API.TNEDIME)
|
|
194
|
+
assert not result["status"]
|
|
195
|
+
assert result["error"] == "No Internet"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_public_ipv4_api_error():
|
|
199
|
+
result = get_public_ipv4(api="api1", geo=True)
|
|
200
|
+
assert not result["status"]
|
|
201
|
+
assert result["error"] == "Unsupported API: api1"
|
|
202
|
+
|
ipspot-0.2/ipspot/functions.py
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""ipspot functions."""
|
|
3
|
-
import argparse
|
|
4
|
-
import socket
|
|
5
|
-
from typing import Union, Dict, Tuple, Any
|
|
6
|
-
import requests
|
|
7
|
-
from art import tprint
|
|
8
|
-
from .params import REQUEST_HEADERS, IPv4API, PARAMETERS_NAME_MAP
|
|
9
|
-
from .params import IPSPOT_OVERVIEW, IPSPOT_REPO, IPSPOT_VERSION
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def ipspot_info() -> None: # pragma: no cover
|
|
13
|
-
"""Print ipspot details."""
|
|
14
|
-
tprint("IPSpot")
|
|
15
|
-
tprint("V:" + IPSPOT_VERSION)
|
|
16
|
-
print(IPSPOT_OVERVIEW)
|
|
17
|
-
print("Repo : " + IPSPOT_REPO)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def get_private_ipv4() -> Dict[str, Union[bool, Dict[str, str], str]]:
|
|
21
|
-
"""Retrieve the private IPv4 address."""
|
|
22
|
-
try:
|
|
23
|
-
hostname = socket.gethostname()
|
|
24
|
-
private_ip = socket.gethostbyname(hostname)
|
|
25
|
-
return {"status": True, "data": {"ip": private_ip}}
|
|
26
|
-
except Exception as e:
|
|
27
|
-
return {"status": False, "error": str(e)}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _ipsb_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
31
|
-
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
32
|
-
"""
|
|
33
|
-
Get public IP and geolocation using ip.sb.
|
|
34
|
-
|
|
35
|
-
:param geo: geolocation flag
|
|
36
|
-
:param timeout: timeout value for API
|
|
37
|
-
"""
|
|
38
|
-
try:
|
|
39
|
-
response = requests.get("https://api.ip.sb/geoip", headers=REQUEST_HEADERS, timeout=timeout)
|
|
40
|
-
response.raise_for_status()
|
|
41
|
-
data = response.json()
|
|
42
|
-
result = {"status": True, "data": {"ip": data.get("ip"), "api": "ip.sb"}}
|
|
43
|
-
if geo:
|
|
44
|
-
geo_data = {
|
|
45
|
-
"city": data.get("city"),
|
|
46
|
-
"region": data.get("region"),
|
|
47
|
-
"country": data.get("country"),
|
|
48
|
-
"country_code": data.get("country_code"),
|
|
49
|
-
"latitude": data.get("latitude"),
|
|
50
|
-
"longitude": data.get("longitude"),
|
|
51
|
-
"organization": data.get("organization"),
|
|
52
|
-
"timezone": data.get("timezone")
|
|
53
|
-
}
|
|
54
|
-
result["data"].update(geo_data)
|
|
55
|
-
return result
|
|
56
|
-
except Exception as e:
|
|
57
|
-
return {"status": False, "error": str(e)}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _ipapi_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
61
|
-
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
62
|
-
"""
|
|
63
|
-
Get public IP and geolocation using ip-api.com.
|
|
64
|
-
|
|
65
|
-
:param geo: geolocation flag
|
|
66
|
-
:param timeout: timeout value for API
|
|
67
|
-
"""
|
|
68
|
-
try:
|
|
69
|
-
response = requests.get("http://ip-api.com/json/", headers=REQUEST_HEADERS, timeout=timeout)
|
|
70
|
-
response.raise_for_status()
|
|
71
|
-
data = response.json()
|
|
72
|
-
|
|
73
|
-
if data.get("status") != "success":
|
|
74
|
-
return {"status": False, "error": "ip-api lookup failed"}
|
|
75
|
-
result = {"status": True, "data": {"ip": data.get("query"), "api": "ip-api.com"}}
|
|
76
|
-
if geo:
|
|
77
|
-
geo_data = {
|
|
78
|
-
"city": data.get("city"),
|
|
79
|
-
"region": data.get("regionName"),
|
|
80
|
-
"country": data.get("country"),
|
|
81
|
-
"country_code": data.get("countryCode"),
|
|
82
|
-
"latitude": data.get("lat"),
|
|
83
|
-
"longitude": data.get("lon"),
|
|
84
|
-
"organization": data.get("org"),
|
|
85
|
-
"timezone": data.get("timezone")
|
|
86
|
-
}
|
|
87
|
-
result["data"].update(geo_data)
|
|
88
|
-
return result
|
|
89
|
-
except Exception as e:
|
|
90
|
-
return {"status": False, "error": str(e)}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _ipinfo_ipv4(geo: bool=False, timeout: Union[float, Tuple[float, float]]
|
|
94
|
-
=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
95
|
-
"""
|
|
96
|
-
Get public IP and geolocation using ipinfo.io.
|
|
97
|
-
|
|
98
|
-
:param geo: geolocation flag
|
|
99
|
-
:param timeout: timeout value for API
|
|
100
|
-
"""
|
|
101
|
-
try:
|
|
102
|
-
response = requests.get("https://ipinfo.io/json", headers=REQUEST_HEADERS, timeout=timeout)
|
|
103
|
-
response.raise_for_status()
|
|
104
|
-
data = response.json()
|
|
105
|
-
result = {"status": True, "data": {"ip": data.get("ip"), "api": "ipinfo.io"}}
|
|
106
|
-
if geo:
|
|
107
|
-
loc = data.get("loc", "").split(",")
|
|
108
|
-
geo_data = {
|
|
109
|
-
"city": data.get("city"),
|
|
110
|
-
"region": data.get("region"),
|
|
111
|
-
"country": None,
|
|
112
|
-
"country_code": data.get("country"),
|
|
113
|
-
"latitude": float(loc[0]) if len(loc) == 2 else None,
|
|
114
|
-
"longitude": float(loc[1]) if len(loc) == 2 else None,
|
|
115
|
-
"organization": data.get("org"),
|
|
116
|
-
"timezone": data.get("timezone")
|
|
117
|
-
}
|
|
118
|
-
result["data"].update(geo_data)
|
|
119
|
-
return result
|
|
120
|
-
except Exception as e:
|
|
121
|
-
return {"status": False, "error": str(e)}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def get_public_ipv4(api: IPv4API=IPv4API.AUTO, geo: bool=False,
|
|
125
|
-
timeout: Union[float, Tuple[float, float]]=5) -> Dict[str, Union[bool, Dict[str, Union[str, float]], str]]:
|
|
126
|
-
"""
|
|
127
|
-
Get public IPv4 and geolocation info based on the selected API.
|
|
128
|
-
|
|
129
|
-
:param api: public IPv4 API
|
|
130
|
-
:param geo: geolocation flag
|
|
131
|
-
:param timeout: timeout value for API
|
|
132
|
-
"""
|
|
133
|
-
api_map = {
|
|
134
|
-
IPv4API.IPAPI: _ipapi_ipv4,
|
|
135
|
-
IPv4API.IPINFO: _ipinfo_ipv4,
|
|
136
|
-
IPv4API.IPSB: _ipsb_ipv4
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if api == IPv4API.AUTO:
|
|
140
|
-
for _, func in api_map.items():
|
|
141
|
-
result = func(geo=geo, timeout=timeout)
|
|
142
|
-
if result["status"]:
|
|
143
|
-
return result
|
|
144
|
-
return {"status": False, "error": "All attempts failed."}
|
|
145
|
-
else:
|
|
146
|
-
func = api_map.get(api)
|
|
147
|
-
if func:
|
|
148
|
-
return func(geo=geo, timeout=timeout)
|
|
149
|
-
return {"status": False, "error": "Unsupported API: {api}".format(api=api)}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def filter_parameter(parameter: Any) -> Any:
|
|
153
|
-
"""
|
|
154
|
-
Filter input parameter.
|
|
155
|
-
|
|
156
|
-
:param parameter: input parameter
|
|
157
|
-
"""
|
|
158
|
-
if parameter is None:
|
|
159
|
-
return "N/A"
|
|
160
|
-
if isinstance(parameter, str) and len(parameter.strip()) == 0:
|
|
161
|
-
return "N/A"
|
|
162
|
-
return parameter
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def display_ip_info(ipv4_api: IPv4API = IPv4API.AUTO, geo: bool=False,
|
|
166
|
-
timeout: Union[float, Tuple[float, float]]=5) -> None: # pragma: no cover
|
|
167
|
-
"""
|
|
168
|
-
Print collected IP and location data.
|
|
169
|
-
|
|
170
|
-
:param ipv4_api: public IPv4 API
|
|
171
|
-
:param geo: geolocation flag
|
|
172
|
-
:param timeout: timeout value for API
|
|
173
|
-
"""
|
|
174
|
-
private_result = get_private_ipv4()
|
|
175
|
-
print("Private IP:\n")
|
|
176
|
-
print(" IP: {private_result[data][ip]}".format(private_result=private_result) if private_result["status"]
|
|
177
|
-
else " Error: {private_result[error]}".format(private_result=private_result))
|
|
178
|
-
|
|
179
|
-
public_title = "\nPublic IP"
|
|
180
|
-
if geo:
|
|
181
|
-
public_title += " and Location Info"
|
|
182
|
-
public_title += ":\n"
|
|
183
|
-
print(public_title)
|
|
184
|
-
public_result = get_public_ipv4(ipv4_api, geo=geo, timeout=timeout)
|
|
185
|
-
if public_result["status"]:
|
|
186
|
-
for name, parameter in sorted(public_result["data"].items()):
|
|
187
|
-
print(
|
|
188
|
-
" {name}: {parameter}".format(
|
|
189
|
-
name=PARAMETERS_NAME_MAP[name],
|
|
190
|
-
parameter=filter_parameter(parameter)))
|
|
191
|
-
else:
|
|
192
|
-
print(" Error: {public_result[error]}".format(public_result=public_result))
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def main() -> None: # pragma: no cover
|
|
196
|
-
"""CLI main function."""
|
|
197
|
-
parser = argparse.ArgumentParser()
|
|
198
|
-
parser.add_argument(
|
|
199
|
-
'--ipv4-api',
|
|
200
|
-
help='public IPv4 API',
|
|
201
|
-
type=str.lower,
|
|
202
|
-
choices=[
|
|
203
|
-
x.value for x in IPv4API],
|
|
204
|
-
default=IPv4API.AUTO.value)
|
|
205
|
-
parser.add_argument('--info', help='info', nargs="?", const=1)
|
|
206
|
-
parser.add_argument('--version', help='version', nargs="?", const=1)
|
|
207
|
-
parser.add_argument('--no-geo', help='no geolocation data', nargs="?", const=1, default=False)
|
|
208
|
-
parser.add_argument('--timeout', help='timeout for the API request', type=float, default=5.0)
|
|
209
|
-
|
|
210
|
-
args = parser.parse_args()
|
|
211
|
-
if args.version:
|
|
212
|
-
print(IPSPOT_VERSION)
|
|
213
|
-
elif args.info:
|
|
214
|
-
ipspot_info()
|
|
215
|
-
else:
|
|
216
|
-
ipv4_api = IPv4API(args.ipv4_api)
|
|
217
|
-
geo = not args.no_geo
|
|
218
|
-
display_ip_info(ipv4_api=ipv4_api, geo=geo, timeout=args.timeout)
|
ipspot-0.2/tests/test_ipv4.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from unittest import mock
|
|
3
|
-
from ipspot import get_private_ipv4
|
|
4
|
-
from ipspot import get_public_ipv4, IPv4API
|
|
5
|
-
|
|
6
|
-
TEST_CASE_NAME = "IPv4 tests"
|
|
7
|
-
IPV4_REGEX = re.compile(r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$')
|
|
8
|
-
DATA_ITEMS = {'country_code', 'latitude', 'longitude', 'api', 'country', 'timezone', 'organization', 'region', 'ip', 'city'}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_private_ipv4_success():
|
|
12
|
-
result = get_private_ipv4()
|
|
13
|
-
assert result["status"]
|
|
14
|
-
assert IPV4_REGEX.match(result["data"]["ip"])
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_private_ipv4_error():
|
|
18
|
-
with mock.patch("socket.gethostbyname", side_effect=Exception("Test error")):
|
|
19
|
-
result = get_private_ipv4()
|
|
20
|
-
assert not result["status"]
|
|
21
|
-
assert result["error"] == "Test error"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_public_ipv4_auto_success():
|
|
25
|
-
result = get_public_ipv4(api=IPv4API.AUTO, geo=True)
|
|
26
|
-
assert result["status"]
|
|
27
|
-
assert IPV4_REGEX.match(result["data"]["ip"])
|
|
28
|
-
assert set(result["data"].keys()) == DATA_ITEMS
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_public_ipv4_auto_timeout_error():
|
|
32
|
-
result = get_public_ipv4(api=IPv4API.AUTO, geo=True, timeout="5")
|
|
33
|
-
assert not result["status"]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_public_ipv4_auto_net_error():
|
|
37
|
-
with mock.patch("requests.get", side_effect=Exception("No Internet")):
|
|
38
|
-
result = get_public_ipv4(api=IPv4API.AUTO)
|
|
39
|
-
assert not result["status"]
|
|
40
|
-
assert result["error"] == "All attempts failed."
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_public_ipv4_ipapi_success():
|
|
44
|
-
result = get_public_ipv4(api=IPv4API.IPAPI, geo=True)
|
|
45
|
-
assert result["status"]
|
|
46
|
-
assert IPV4_REGEX.match(result["data"]["ip"])
|
|
47
|
-
assert set(result["data"].keys()) == DATA_ITEMS
|
|
48
|
-
assert result["data"]["api"] == "ip-api.com"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_public_ipv4_ipapi_timeout_error():
|
|
52
|
-
result = get_public_ipv4(api=IPv4API.IPAPI, geo=True, timeout="5")
|
|
53
|
-
assert not result["status"]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_public_ipv4_ipapi_net_error():
|
|
57
|
-
with mock.patch("requests.get", side_effect=Exception("No Internet")):
|
|
58
|
-
result = get_public_ipv4(api=IPv4API.IPAPI)
|
|
59
|
-
assert not result["status"]
|
|
60
|
-
assert result["error"] == "No Internet"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_public_ipv4_ipinfo_success():
|
|
64
|
-
result = get_public_ipv4(api=IPv4API.IPINFO, geo=True)
|
|
65
|
-
assert result["status"]
|
|
66
|
-
assert IPV4_REGEX.match(result["data"]["ip"])
|
|
67
|
-
assert set(result["data"].keys()) == DATA_ITEMS
|
|
68
|
-
assert result["data"]["api"] == "ipinfo.io"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def test_public_ipv4_ipinfo_timeout_error():
|
|
72
|
-
result = get_public_ipv4(api=IPv4API.IPINFO, geo=True, timeout="5")
|
|
73
|
-
assert not result["status"]
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def test_public_ipv4_ipinfo_net_error():
|
|
77
|
-
with mock.patch("requests.get", side_effect=Exception("No Internet")):
|
|
78
|
-
result = get_public_ipv4(api=IPv4API.IPINFO)
|
|
79
|
-
assert not result["status"]
|
|
80
|
-
assert result["error"] == "No Internet"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_public_ipv4_ipsb_success():
|
|
84
|
-
result = get_public_ipv4(api=IPv4API.IPSB, geo=True)
|
|
85
|
-
assert result["status"]
|
|
86
|
-
assert IPV4_REGEX.match(result["data"]["ip"])
|
|
87
|
-
assert set(result["data"].keys()) == DATA_ITEMS
|
|
88
|
-
assert result["data"]["api"] == "ip.sb"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def test_public_ipv4_ipsb_timeout_error():
|
|
92
|
-
result = get_public_ipv4(api=IPv4API.IPSB, geo=True, timeout="5")
|
|
93
|
-
assert not result["status"]
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def test_public_ipv4_ipsb_net_error():
|
|
98
|
-
with mock.patch("requests.get", side_effect=Exception("No Internet")):
|
|
99
|
-
result = get_public_ipv4(api=IPv4API.IPSB)
|
|
100
|
-
assert not result["status"]
|
|
101
|
-
assert result["error"] == "No Internet"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def test_public_ipv4_api_error():
|
|
105
|
-
result = get_public_ipv4(api="api1", geo=True)
|
|
106
|
-
assert not result["status"]
|
|
107
|
-
assert result["error"] == "Unsupported API: api1"
|
|
108
|
-
|
|
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
|