Coverage for jutil/request.py: 52%
88 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-07 16:40 -0500
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-07 16:40 -0500
1import logging
2from typing import Tuple, Union, Optional
3from django.conf import settings
4import requests
5import socket
6from django.http.request import HttpRequest
7from ipware import get_client_ip # type: ignore
9try:
10 from rest_framework.request import Request # type: ignore
11except Exception as err:
12 raise Exception("Using jutil.request requires djangorestframework installed") from err
14logger = logging.getLogger(__name__)
17class GeoIP:
18 """
19 Basic geolocation information of IP-address.
20 """
22 ip: str
23 country_name: str
24 country_code: str
25 time_zone: str
26 city: str
27 zip_code: str
28 latitude: float
29 longitude: float
31 def __init__( # pylint: disable=too-many-arguments
32 self,
33 ip: str,
34 country_name: str,
35 country_code: str,
36 time_zone: str,
37 city: str,
38 zip_code: str,
39 latitude: float,
40 longitude: float,
41 ):
42 self.ip = ip
43 self.country_name = country_name
44 self.country_code = country_code
45 self.time_zone = time_zone
46 self.city = city
47 self.zip_code = zip_code
48 self.latitude = latitude
49 self.longitude = longitude
52def get_ip(request: Union[HttpRequest, Request]) -> str:
53 """
54 Returns best-guess IP for given request.
55 Uses ipware library get_client_ip.
56 If you need to know is IP routable or not, use ipware get_client_ip directly.
57 See ipware documentation for more info.
59 Note: Why such a simple function wrapper? I'm generally against wrappers like this,
60 but in this case made an exceptions: I used to use ipware get_real_ip() everywhere before
61 it was deprecated and had quite big update process to change all code to use ipware get_client_ip.
62 I want to avoid such process again so added this wrapper.
64 :param request: Django's HttpRequest or DRF Request
65 :return: IP-address or None
66 """
67 return get_client_ip(request)[0]
70def get_geo_ip_from_ipgeolocation(ip: str, timeout: int = 10, verbose: bool = False) -> GeoIP:
71 """
72 Returns geo IP info or empty dict if geoip query fails.
73 Uses ipgeolocation.io API and requires settings.IPGEOLOCATION_API_KEY set.
75 :param ip: str
76 :param timeout: timeout in seconds
77 :return: IPGeoInfo
78 """
79 if not hasattr(settings, "IPGEOLOCATION_API_KEY") or not settings.IPGEOLOCATION_API_KEY: 79 ↛ 80line 79 didn't jump to line 80, because the condition on line 79 was never true
80 raise Exception("get_geo_ip_ipstack() requires IPGEOLOCATION_API_KEY defined in Django settings")
81 url = f"https://api.ipgeolocation.io/ipgeo?apiKey={settings.IPGEOLOCATION_API_KEY}&ip={ip}"
82 res = requests.get(url, timeout=timeout)
83 if verbose: 83 ↛ 84line 83 didn't jump to line 84, because the condition on line 83 was never true
84 logger.info("GET %s HTTP %s: %s", url, res.status_code, res.text)
85 if res.status_code != 200: 85 ↛ 86line 85 didn't jump to line 86, because the condition on line 85 was never true
86 logger.error("get_geo_ip_from_ipgeolocation(%s) failed: %s", ip, res.text)
87 raise Exception("api.ipgeolocation.io HTTP {}".format(res.status_code))
88 data = res.json()
89 return GeoIP(
90 ip,
91 country_name=data["country_name"],
92 country_code=data["country_code2"],
93 time_zone=(data.get("time_zone") or {}).get("name") or "",
94 city=data.get("city") or "",
95 zip_code=data.get("zipcode") or "",
96 latitude=float(data["latitude"]),
97 longitude=float(data["longitude"]),
98 )
101def get_geo_ip_from_ipstack(ip: str, timeout: int = 10, verbose: bool = False) -> GeoIP:
102 """
103 Returns geo IP info or empty dict if geoip query fails.
104 Uses ipstack.com API and requires settings.IPSTACK_TOKEN set.
106 :param ip: str
107 :param timeout: timeout in seconds
108 :return: IPGeoInfo
109 """
110 if not hasattr(settings, "IPSTACK_TOKEN") or not settings.IPSTACK_TOKEN:
111 raise Exception("get_geo_ip_from_ipstack() requires IPSTACK_TOKEN defined in Django settings")
112 url = f"http://api.ipstack.com/{ip}?access_key={settings.IPSTACK_TOKEN}&format=1"
113 res = requests.get(url, timeout=timeout)
114 if verbose:
115 logger.info("GET %s HTTP %s: %s", url, res.status_code, res.text)
116 if res.status_code != 200:
117 logger.error("get_geo_ip_from_ipstack(%s) failed: %s", ip, res.text)
118 raise Exception("api.ipstack.com HTTP {}".format(res.status_code))
119 data = res.json()
120 res_success = data.get("success", True)
121 if not res_success:
122 res_info = data.get("info") or ""
123 logger.error("get_geo_ip_from_ipstack(%s) failed: %s", ip, res_info)
124 raise Exception(res_info)
125 return GeoIP(
126 ip,
127 country_name=data["country_name"],
128 country_code=data["country_code"],
129 time_zone=data.get("time_zone") or "",
130 city=data.get("city") or "",
131 zip_code=data.get("zip") or "",
132 latitude=float(data["latitude"]),
133 longitude=float(data["longitude"]),
134 )
137def get_geo_ip(ip: str, timeout: int = 10, verbose: bool = False) -> GeoIP:
138 """
139 Returns geo IP info. Raises Exception if query fails.
140 Uses either ipgeolocation.io (if IPGEOLOCATION_API_KEY set) or ipstack.com (if IPSTACK_TOKEN set)
142 Example response (GeoIP.__dict__):
144 {
145 "ip": "194.100.27.41",
146 "country_name": "Finland",
147 "country_code": "FI",
148 "time_zone": "Europe/Helsinki",
149 "city": "Helsinki",
150 "zip_code": "00100",
151 "latitude": "60.17116",
152 "longitude": "24.93265"
153 }
155 :param ip: str
156 :param timeout: timeout in seconds
157 :return: IPGeoInfo
158 """
159 if hasattr(settings, "IPGEOLOCATION_API_KEY") and settings.IPGEOLOCATION_API_KEY: 159 ↛ 161line 159 didn't jump to line 161, because the condition on line 159 was never false
160 return get_geo_ip_from_ipgeolocation(ip, timeout, verbose=verbose)
161 if hasattr(settings, "IPSTACK_TOKEN") and settings.IPSTACK_TOKEN:
162 return get_geo_ip_from_ipstack(ip, timeout, verbose=verbose)
163 raise Exception("get_geo_ip() requires either IPGEOLOCATION_TOKEN or IPSTACK_TOKEN defined in Django settings")
166def get_geo_ip_or_none(ip: str, timeout: int = 10) -> Optional[GeoIP]:
167 """
168 Returns geo IP info or None if geoip query fails.
169 Uses either ipgeolocation.io (if IPGEOLOCATION_API_KEY set) or ipstack.com (if IPSTACK_TOKEN set)
171 :param ip: str
172 :param timeout: timeout in seconds
173 :return: Optional[IPGeoInfo]
174 """
175 try:
176 return get_geo_ip(ip, timeout)
177 except Exception as err:
178 logger.error("get_geo_ip_or_none(%s) failed: %s", ip, err)
179 return None
182def get_ip_info(ip: str, exceptions: bool = False, timeout: int = 10) -> Tuple[str, str, str]:
183 """
184 Returns (ip, country_code, host) tuple of the IP address.
185 Uses either ipgeolocation.io (if IPGEOLOCATION_API_KEY set) or ipstack.com (if IPSTACK_TOKEN set)
187 :param ip: IP address
188 :param exceptions: Raise Exception or not
189 :param timeout: Timeout in seconds. Note that timeout only affects geo IP part, not getting host name.
190 :return: (ip, country_code, host)
191 """
192 if not ip: # localhost 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true
193 return "", "", ""
194 host, country_code = "", ""
195 try:
196 geo = get_geo_ip(ip, timeout=timeout)
197 country_code = geo.country_code
198 if ip: 198 ↛ 205line 198 didn't jump to line 205, because the condition on line 198 was never false
199 host_info = socket.gethostbyaddr(ip)
200 host = host_info[0][:255]
201 except Exception as e:
202 logger.error("get_ip_info(%s) failed: %s", ip, e)
203 if exceptions:
204 raise e
205 return ip, country_code, host