Skip to content

Commit 5fe6a57

Browse files
committed
Add HandlerLite and AsyncHandlerLite to support Lite API
1 parent dcb452b commit 5fe6a57

6 files changed

Lines changed: 562 additions & 0 deletions

File tree

ipinfo/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from .handler_lite import HandlerLite
2+
from .handler_lite_async import AsyncHandlerLite
13
from .handler import Handler
24
from .handler_async import AsyncHandler
35

@@ -7,6 +9,16 @@ def getHandler(access_token=None, **kwargs):
79
return Handler(access_token, **kwargs)
810

911

12+
def getHandlerLite(access_token=None, **kwargs):
13+
"""Create and return HandlerLite object."""
14+
return HandlerLite(access_token, **kwargs)
15+
16+
1017
def getHandlerAsync(access_token=None, **kwargs):
1118
"""Create an return an asynchronous Handler object."""
1219
return AsyncHandler(access_token, **kwargs)
20+
21+
22+
def getHandlerAsyncLite(access_token=None, **kwargs):
23+
"""Create and return asynchronous HandlerLite object."""
24+
return AsyncHandlerLite(access_token, **kwargs)

ipinfo/handler_lite.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""
2+
Main API client handler for fetching data from the IPinfo service.
3+
"""
4+
5+
from ipaddress import IPv4Address, IPv6Address
6+
7+
import requests
8+
9+
from .error import APIError
10+
from .cache.default import DefaultCache
11+
from .details import Details
12+
from .exceptions import RequestQuotaExceededError
13+
from .handler_utils import (
14+
LITE_API_URL,
15+
CACHE_MAXSIZE,
16+
CACHE_TTL,
17+
REQUEST_TIMEOUT_DEFAULT,
18+
cache_key,
19+
)
20+
from . import handler_utils
21+
from .bogon import is_bogon
22+
from .data import (
23+
continents,
24+
countries,
25+
countries_currencies,
26+
eu_countries,
27+
countries_flags,
28+
)
29+
30+
31+
class HandlerLite:
32+
"""
33+
Allows client to request data for specified IP address using the Lite API.
34+
Instantiates and maintains access to cache.
35+
"""
36+
37+
def __init__(self, access_token=None, **kwargs):
38+
"""
39+
Initialize the Handler object with country name list and the
40+
cache initialized.
41+
"""
42+
self.access_token = access_token
43+
44+
# load countries file
45+
self.countries = kwargs.get("countries") or countries
46+
47+
# load eu countries file
48+
self.eu_countries = kwargs.get("eu_countries") or eu_countries
49+
50+
# load countries flags file
51+
self.countries_flags = kwargs.get("countries_flags") or countries_flags
52+
53+
# load countries currency file
54+
self.countries_currencies = (
55+
kwargs.get("countries_currencies") or countries_currencies
56+
)
57+
58+
# load continent file
59+
self.continents = kwargs.get("continent") or continents
60+
61+
# setup req opts
62+
self.request_options = kwargs.get("request_options", {})
63+
if "timeout" not in self.request_options:
64+
self.request_options["timeout"] = REQUEST_TIMEOUT_DEFAULT
65+
66+
# setup cache
67+
if "cache" in kwargs:
68+
self.cache = kwargs["cache"]
69+
else:
70+
cache_options = kwargs.get("cache_options", {})
71+
if "maxsize" not in cache_options:
72+
cache_options["maxsize"] = CACHE_MAXSIZE
73+
if "ttl" not in cache_options:
74+
cache_options["ttl"] = CACHE_TTL
75+
self.cache = DefaultCache(**cache_options)
76+
77+
# setup custom headers
78+
self.headers = kwargs.get("headers", None)
79+
80+
def getDetails(self, ip_address=None, timeout=None):
81+
"""
82+
Get details for specified IP address as a Details object.
83+
84+
If `timeout` is not `None`, it will override the client-level timeout
85+
just for this operation.
86+
"""
87+
# If the supplied IP address uses the objects defined in the built-in
88+
# module ipaddress extract the appropriate string notation before
89+
# formatting the URL.
90+
if isinstance(ip_address, IPv4Address) or isinstance(ip_address, IPv6Address):
91+
ip_address = ip_address.exploded
92+
93+
# check if bogon.
94+
if ip_address and is_bogon(ip_address):
95+
details = {}
96+
details["ip"] = ip_address
97+
details["bogon"] = True
98+
return Details(details)
99+
100+
# check cache first.
101+
try:
102+
cached_ipaddr = self.cache[cache_key(ip_address)]
103+
return Details(cached_ipaddr)
104+
except KeyError:
105+
pass
106+
107+
# prepare req http opts
108+
req_opts = {**self.request_options}
109+
if timeout is not None:
110+
req_opts["timeout"] = timeout
111+
112+
# not in cache; do http req
113+
url = f"{LITE_API_URL}/{ip_address}" if ip_address else f"{LITE_API_URL}/me"
114+
headers = handler_utils.get_headers(self.access_token, self.headers)
115+
response = requests.get(url, headers=headers, **req_opts)
116+
if response.status_code == 429:
117+
raise RequestQuotaExceededError()
118+
if response.status_code >= 400:
119+
error_code = response.status_code
120+
content_type = response.headers.get("Content-Type")
121+
if content_type == "application/json":
122+
error_response = response.json()
123+
else:
124+
error_response = {"error": response.text}
125+
raise APIError(error_code, error_response)
126+
details = response.json()
127+
128+
# format & cache
129+
handler_utils.format_details(
130+
details,
131+
self.countries,
132+
self.eu_countries,
133+
self.countries_flags,
134+
self.countries_currencies,
135+
self.continents,
136+
)
137+
self.cache[cache_key(ip_address)] = details
138+
139+
return Details(details)

ipinfo/handler_lite_async.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""
2+
Main API client asynchronous handler for fetching data from the IPinfo service.
3+
"""
4+
5+
from ipaddress import IPv4Address, IPv6Address
6+
7+
import aiohttp
8+
9+
from .error import APIError
10+
from .cache.default import DefaultCache
11+
from .details import Details
12+
from .exceptions import RequestQuotaExceededError
13+
from .handler_utils import (
14+
CACHE_MAXSIZE,
15+
CACHE_TTL,
16+
LITE_API_URL,
17+
REQUEST_TIMEOUT_DEFAULT,
18+
cache_key,
19+
)
20+
from . import handler_utils
21+
from .bogon import is_bogon
22+
from .data import (
23+
continents,
24+
countries,
25+
countries_currencies,
26+
eu_countries,
27+
countries_flags,
28+
)
29+
30+
31+
class AsyncHandlerLite:
32+
"""
33+
Allows client to request data for specified IP address asynchronously using the Lite API.
34+
Instantiates and maintains access to cache.
35+
"""
36+
37+
def __init__(self, access_token=None, **kwargs):
38+
"""
39+
Initialize the Handler object with country name list and the
40+
cache initialized.
41+
"""
42+
self.access_token = access_token
43+
44+
# load countries file
45+
self.countries = kwargs.get("countries") or countries
46+
47+
# load eu countries file
48+
self.eu_countries = kwargs.get("eu_countries") or eu_countries
49+
50+
# load countries flags file
51+
self.countries_flags = kwargs.get("countries_flags") or countries_flags
52+
53+
# load countries currency file
54+
self.countries_currencies = (
55+
kwargs.get("countries_currencies") or countries_currencies
56+
)
57+
58+
# load continent file
59+
self.continents = kwargs.get("continent") or continents
60+
61+
# setup req opts
62+
self.request_options = kwargs.get("request_options", {})
63+
if "timeout" not in self.request_options:
64+
self.request_options["timeout"] = REQUEST_TIMEOUT_DEFAULT
65+
66+
# setup aiohttp
67+
self.httpsess = None
68+
69+
# setup cache
70+
if "cache" in kwargs:
71+
self.cache = kwargs["cache"]
72+
else:
73+
cache_options = kwargs.get("cache_options", {})
74+
if "maxsize" not in cache_options:
75+
cache_options["maxsize"] = CACHE_MAXSIZE
76+
if "ttl" not in cache_options:
77+
cache_options["ttl"] = CACHE_TTL
78+
self.cache = DefaultCache(**cache_options)
79+
80+
# setup custom headers
81+
self.headers = kwargs.get("headers", None)
82+
83+
async def init(self):
84+
"""
85+
Initializes internal aiohttp connection pool.
86+
87+
This isn't _required_, as the pool is initialized lazily when needed.
88+
But in case you require non-lazy initialization, you may await this.
89+
90+
This is idempotent.
91+
"""
92+
await self._ensure_aiohttp_ready()
93+
94+
async def deinit(self):
95+
"""
96+
Deinitialize the async handler.
97+
98+
This is required in case you need to let go of the memory/state
99+
associated with the async handler in a long-running process.
100+
101+
This is idempotent.
102+
"""
103+
if self.httpsess:
104+
await self.httpsess.close()
105+
self.httpsess = None
106+
107+
async def getDetails(self, ip_address=None, timeout=None):
108+
"""Get details for specified IP address as a Details object."""
109+
self._ensure_aiohttp_ready()
110+
111+
# If the supplied IP address uses the objects defined in the built-in
112+
# module ipaddress, extract the appropriate string notation before
113+
# formatting the URL.
114+
if isinstance(ip_address, IPv4Address) or isinstance(ip_address, IPv6Address):
115+
ip_address = ip_address.exploded
116+
117+
# check if bogon.
118+
if ip_address and is_bogon(ip_address):
119+
details = {"ip": ip_address, "bogon": True}
120+
return Details(details)
121+
122+
# check cache first.
123+
try:
124+
cached_ipaddr = self.cache[cache_key(ip_address)]
125+
return Details(cached_ipaddr)
126+
except KeyError:
127+
pass
128+
129+
# not in cache; do http req
130+
url = f"{LITE_API_URL}/{ip_address}" if ip_address else f"{LITE_API_URL}/me"
131+
headers = handler_utils.get_headers(self.access_token, self.headers)
132+
req_opts = {}
133+
if timeout is not None:
134+
req_opts["timeout"] = timeout
135+
async with self.httpsess.get(url, headers=headers, **req_opts) as resp:
136+
if resp.status == 429:
137+
raise RequestQuotaExceededError()
138+
if resp.status >= 400:
139+
error_code = resp.status
140+
content_type = resp.headers.get("Content-Type")
141+
if content_type == "application/json":
142+
error_response = await resp.json()
143+
else:
144+
error_response = {"error": resp.text()}
145+
raise APIError(error_code, error_response)
146+
details = await resp.json()
147+
148+
# format & cache
149+
handler_utils.format_details(
150+
details,
151+
self.countries,
152+
self.eu_countries,
153+
self.countries_flags,
154+
self.countries_currencies,
155+
self.continents,
156+
)
157+
self.cache[cache_key(ip_address)] = details
158+
159+
return Details(details)
160+
161+
def _ensure_aiohttp_ready(self):
162+
"""Ensures aiohttp internal state is initialized."""
163+
if self.httpsess:
164+
return
165+
166+
timeout = aiohttp.ClientTimeout(total=self.request_options["timeout"])
167+
self.httpsess = aiohttp.ClientSession(timeout=timeout)

ipinfo/handler_utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
# Base URL to make requests against.
1313
API_URL = "https://ipinfo.io"
1414

15+
# Base URL for the IPinfo Lite API
16+
LITE_API_URL = "https://api.ipinfo.io/lite"
17+
1518
# Base URL to get country flag image link.
1619
# "PK" -> "https://cdn.ipinfo.io/static/images/countries-flags/PK.svg"
1720
COUNTRY_FLAGS_URL = "https://cdn.ipinfo.io/static/images/countries-flags/"

0 commit comments

Comments
 (0)