Skip to content

Commit 8e00a4d

Browse files
committed
initial commit of resproxy handlers
1 parent ff42b7a commit 8e00a4d

File tree

4 files changed

+533
-0
lines changed

4 files changed

+533
-0
lines changed

ipinfo/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from .handler_core_async import AsyncHandlerCore
77
from .handler_plus import HandlerPlus
88
from .handler_plus_async import AsyncHandlerPlus
9+
from .handler_resproxy import HandlerResProxy
10+
from .handler_resproxy_async import AsyncHandlerResProxy
911

1012

1113
def getHandler(access_token=None, **kwargs):
@@ -28,6 +30,11 @@ def getHandlerPlus(access_token=None, **kwargs):
2830
return HandlerPlus(access_token, **kwargs)
2931

3032

33+
def getHandlerResProxy(access_token=None, **kwargs):
34+
"""Create and return HandlerResProxy object."""
35+
return HandlerResProxy(access_token, **kwargs)
36+
37+
3138
def getHandlerAsync(access_token=None, **kwargs):
3239
"""Create an return an asynchronous Handler object."""
3340
return AsyncHandler(access_token, **kwargs)
@@ -46,3 +53,8 @@ def getHandlerAsyncCore(access_token=None, **kwargs):
4653
def getHandlerAsyncPlus(access_token=None, **kwargs):
4754
"""Create and return asynchronous HandlerPlus object."""
4855
return AsyncHandlerPlus(access_token, **kwargs)
56+
57+
58+
def getHandlerAsyncResProxy(access_token=None, **kwargs):
59+
"""Create and return asynchronous HandlerResProxy object."""
60+
return AsyncHandlerResProxy(access_token, **kwargs)

ipinfo/handler_resproxy.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
"""
2+
Residential Proxy API client handler for fetching data from the IPinfo Residential Proxy service.
3+
"""
4+
5+
import re
6+
import time
7+
from ipaddress import IPv4Address, IPv6Address
8+
9+
import requests
10+
11+
from . import handler_utils
12+
from .bogon import is_bogon
13+
from .cache.default import DefaultCache
14+
from .details import Details
15+
from .error import APIError
16+
from .exceptions import RequestQuotaExceededError, TimeoutExceededError
17+
from .handler_utils import (
18+
BATCH_MAX_SIZE,
19+
BATCH_REQ_TIMEOUT_DEFAULT,
20+
CACHE_MAXSIZE,
21+
CACHE_TTL,
22+
RESPROXY_API_URL,
23+
REQUEST_TIMEOUT_DEFAULT,
24+
cache_key,
25+
)
26+
27+
28+
class HandlerResProxy:
29+
"""
30+
Allows client to request data for specified IP address using the ResProxy API.
31+
ResProxy API provides contextual data around Residential, Mobile, and
32+
Datacenter proxies.
33+
Instantiates and maintains access to cache.
34+
"""
35+
36+
def __init__(self, access_token=None, **kwargs):
37+
"""
38+
Initialize the HandlerResProxy object with the cache initialized.
39+
"""
40+
self.access_token = access_token
41+
42+
# setup request options
43+
self.request_options = kwargs.get("request_options", {})
44+
if "timeout" not in self.request_options:
45+
self.request_options["timeout"] = REQUEST_TIMEOUT_DEFAULT
46+
47+
# setup cache
48+
if "cache" in kwargs:
49+
self.cache = kwargs["cache"]
50+
else:
51+
cache_options = kwargs.get("cache_options", {})
52+
if "maxsize" not in cache_options:
53+
cache_options["maxsize"] = CACHE_MAXSIZE
54+
if "ttl" not in cache_options:
55+
cache_options["ttl"] = CACHE_TTL
56+
self.cache = DefaultCache(**cache_options)
57+
58+
# set up custom headers
59+
self.headers = kwargs.get("headers", None)
60+
61+
def getDetails(self, ip_address=None, timeout=None):
62+
"""
63+
Get ResProxy details for the specified IP as a Details object.
64+
65+
If `timeout` is not `None`, it will override the client-level timeout
66+
just for this operation.
67+
"""
68+
# If the supplied IP address uses the object defined in the built-in
69+
# module ipaddress extract the appropriate string notation before
70+
# formatting the URL.
71+
if isinstance(ip_address, (IPv4Address, IPv6Address)):
72+
ip_address = ip_address.exploded
73+
74+
# check if bogon.
75+
if ip_address and is_bogon(ip_address):
76+
details = {}
77+
details["ip"] = ip_address
78+
details["bogon"] = True
79+
return Details(details)
80+
81+
# check cache first.
82+
try:
83+
cached_data = self.cache[cache_key(ip_address)]
84+
return Details(cached_data)
85+
except KeyError:
86+
pass
87+
88+
# prepare request http options
89+
req_opts = {**self.request_options}
90+
if timeout is not None:
91+
req_opts["timeout"] = timeout
92+
93+
# Build URL
94+
url = RESPROXY_API_URL
95+
if ip_address:
96+
url += "/" + ip_address
97+
98+
headers = handler_utils.get_headers(self.access_token, self.headers)
99+
response = requests.get(url, headers=headers, **req_opts)
100+
101+
if response.status_code == 429:
102+
raise RequestQuotaExceededError()
103+
if response.status_code >= 400:
104+
error_code = response.status_code
105+
content_type = response.headers.get("Content-Type")
106+
if content_type == "application/json":
107+
error_response = response.json()
108+
else:
109+
error_response = {"error": response.text}
110+
raise APIError(error_code, error_response)
111+
112+
details = response.json()
113+
114+
# add cache
115+
self.cache[cache_key(ip_address)] = details
116+
117+
return Details(details)
118+
119+
def getBatchDetails(
120+
self,
121+
ip_addresses,
122+
batch_size=None,
123+
timeout_per_batch=BATCH_REQ_TIMEOUT_DEFAULT,
124+
timeout_total=None,
125+
raise_on_fail=True,
126+
):
127+
"""
128+
Get ResProxy details for a batch of IP addresses at once.
129+
130+
There is no specified limit to the number of IPs this function can
131+
accept; it can handle as much as the user can fit in RAM (along with
132+
all of the response data, which is at least a magnitude larger than the
133+
input list).
134+
135+
The input list is broken up into batches to abide by API requirements.
136+
The batch size can be adjusted with `batch_size` but is clipped to
137+
`BATCH_MAX_SIZE`.
138+
Defaults to `BATCH_MAX_SIZE`.
139+
140+
For each batch, `timeout_per_batch` indicates the maximum seconds to
141+
spend waiting for the HTTP request to complete. If any batch fails with
142+
this timeout, the whole operation fails.
143+
Defaults to `BATCH_REQ_TIMEOUT_DEFAULT` seconds.
144+
145+
`timeout_total` is a seconds-denominated hard-timeout for the time
146+
spent in HTTP operations; regardless of whether all batches have
147+
succeeded so far, if `timeout_total` is reached, the whole operation
148+
will fail by raising `TimeoutExceededError`.
149+
Defaults to being turned off.
150+
151+
`raise_on_fail`, if turned off, will return any result retrieved so far
152+
rather than raise an exception when errors occur, including timeout and
153+
quota errors.
154+
Defaults to on.
155+
"""
156+
if batch_size is None:
157+
batch_size = BATCH_MAX_SIZE
158+
159+
result = {}
160+
lookup_addresses = []
161+
162+
# pre-populate with anything we've got in the cache, and keep around
163+
# the IPs not in the cache.
164+
for ip_address in ip_addresses:
165+
if isinstance(ip_address, (IPv4Address, IPv6Address)):
166+
ip_address = ip_address.exploded
167+
168+
if ip_address and is_bogon(ip_address):
169+
details = {}
170+
details["ip"] = ip_address
171+
details["bogon"] = True
172+
result[ip_address] = Details(details)
173+
else:
174+
try:
175+
cached_data = self.cache[cache_key(ip_address)]
176+
result[ip_address] = Details(cached_data)
177+
except KeyError:
178+
# prepend the url pattern for bulk lookup operations
179+
lookup_addresses.append(f"resproxy/{ip_address}")
180+
181+
if len(lookup_addresses) == 0:
182+
return result
183+
184+
if timeout_total is not None:
185+
start_time = time.time()
186+
187+
# prepare req options
188+
req_opts = {**self.request_options, "timeout": timeout_per_batch}
189+
190+
# loop over batch chunks and do lookup for each.
191+
url = "https://api.ipinfo.io/batch"
192+
headers = handler_utils.get_headers(self.access_token, self.headers)
193+
headers["content-type"] = "application/json"
194+
195+
for i in range(0, len(lookup_addresses), batch_size):
196+
# quit if total timeout is reached
197+
if timeout_total is not None and time.time() - start_time > timeout_total:
198+
return handler_utils.return_or_fail(
199+
raise_on_fail, TimeoutExceededError(), result
200+
)
201+
202+
chunk = lookup_addresses[i : i + batch_size]
203+
204+
# lookup
205+
try:
206+
response = requests.post(url, json=chunk, headers=headers, **req_opts)
207+
except Exception as e:
208+
return handler_utils.return_or_fail(raise_on_fail, e, result)
209+
210+
try:
211+
if response.status_code == 429:
212+
raise RequestQuotaExceededError()
213+
response.raise_for_status()
214+
except Exception as e:
215+
return handler_utils.return_or_fail(raise_on_fail, e, result)
216+
217+
json_response = response.json()
218+
219+
for ip_address, data in json_response.items():
220+
unwound_ip_address = re.sub(r"^resproxy\/", "", ip_address)
221+
# Cache and format the data
222+
self.cache[cache_key(unwound_ip_address)] = data
223+
result[unwound_ip_address] = Details(data)
224+
225+
return result

0 commit comments

Comments
 (0)