Skip to content

Commit fd79139

Browse files
authored
Merge pull request #110 from silvanocerza/lite-api
Add `HandlerLite` and `AsyncHandlerLite` to support Lite API
2 parents dcb452b + 86ad9bd commit fd79139

7 files changed

Lines changed: 584 additions & 5 deletions

File tree

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
This is the official Python client library for the IPinfo.io IP address API, allowing you to look up your own IP address, or get any of the following details for an IP:
44

5-
- [IP geolocation](https://ipinfo.io/ip-geolocation-api) (city, region, country, postal code, latitude, and longitude)
6-
- [ASN details](https://ipinfo.io/asn-api) (ISP or network operator, associated domain name, and type, such as business, hosting, or company)
7-
- [Firmographics data](https://ipinfo.io/ip-company-api) (the name and domain of the business that uses the IP address)
8-
- [Carrier information](https://ipinfo.io/ip-carrier-api) (the name of the mobile carrier and MNC and MCC for that carrier if the IP is used exclusively for mobile traffic)
5+
- [IP geolocation](https://ipinfo.io/ip-geolocation-api) (city, region, country, postal code, latitude, and longitude)
6+
- [ASN details](https://ipinfo.io/asn-api) (ISP or network operator, associated domain name, and type, such as business, hosting, or company)
7+
- [Firmographics data](https://ipinfo.io/ip-company-api) (the name and domain of the business that uses the IP address)
8+
- [Carrier information](https://ipinfo.io/ip-carrier-api) (the name of the mobile carrier and MNC and MCC for that carrier if the IP is used exclusively for mobile traffic)
99

1010
## Getting Started
1111

1212
You'll need an IPinfo API access token, which you can get by signing up for a free account at [https://ipinfo.io/signup](https://ipinfo.io/signup).
1313

1414
The free plan is limited to 50,000 requests per month, and doesn't include some of the data fields such as IP type and company data. To enable all the data fields and additional request volumes see [https://ipinfo.io/pricing](https://ipinfo.io/pricing)
1515

16-
⚠️ Note: This library does not currently support our newest free API https://ipinfo.io/lite. If you’d like to use IPinfo Lite, you can call the [endpoint directly](https://ipinfo.io/developers/lite-api) using your preferred HTTP client. Developers are also welcome to contribute support for Lite by submitting a pull request.
16+
The library also supports the Lite API, see the [Lite API section](#lite-api) for more info.
1717

1818
### Installation
1919

@@ -162,6 +162,22 @@ The IPinfo library can be authenticated with your IPinfo API token, which is pas
162162
'timezone': 'America/Los_Angeles'}
163163
```
164164

165+
### Lite API
166+
167+
The library gives the possibility to use the [Lite API](https://ipinfo.io/developers/lite-api) too, authentication with your token is still required.
168+
169+
The returned details are slightly different from the Core API.
170+
171+
```python
172+
>>> import ipinfo
173+
>>> handler = ipinfo.getHandlerLite(access_token='123456789abc')
174+
>>> details = handler.getDetails("8.8.8.8")
175+
>>> details.country_code
176+
'US'
177+
>>> details.country
178+
'United States'
179+
```
180+
165181
### Caching
166182

167183
In-memory caching of `details` data is provided by default via the [cachetools](https://cachetools.readthedocs.io/en/latest/) library. This uses an LRU (least recently used) cache with a TTL (time to live) by default. This means that values will be cached for the specified duration; if the cache's max size is reached, cache values will be invalidated as necessary, starting with the oldest cached value.
@@ -297,6 +313,7 @@ When looking up an IP address, the response object includes `details.country_nam
297313
continents=continents
298314
)
299315
```
316+
300317
### Batch Operations
301318

302319
Looking up a single IP at a time can be slow. It could be done concurrently

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)