Skip to content

Commit 6a97650

Browse files
authored
Merge pull request #82 from ipinfo/iterative-batch-ips
Adding iterative batch function for IPs
2 parents d3e7eb9 + 29d114e commit 6a97650

File tree

5 files changed

+194
-2
lines changed

5 files changed

+194
-2
lines changed

ipinfo/handler.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,10 @@ def getBatchDetails(
201201
batch_size = BATCH_MAX_SIZE
202202

203203
result = {}
204+
lookup_addresses = []
204205

205206
# pre-populate with anything we've got in the cache, and keep around
206207
# the IPs not in the cache.
207-
lookup_addresses = []
208208
for ip_address in ip_addresses:
209209
# if the supplied IP address uses the objects defined in the
210210
# built-in module ipaddress extract the appropriate string notation
@@ -214,6 +214,14 @@ def getBatchDetails(
214214
):
215215
ip_address = ip_address.exploded
216216

217+
if ip_address and is_bogon(ip_address):
218+
details = {}
219+
details["ip"] = ip_address
220+
details["bogon"] = True
221+
result[ip_address] = Details(details)
222+
else:
223+
lookup_addresses.append(ip_address)
224+
217225
try:
218226
cached_ipaddr = self.cache[cache_key(ip_address)]
219227
result[ip_address] = cached_ipaddr
@@ -309,3 +317,72 @@ def getMap(self, ips):
309317
)
310318
response.raise_for_status()
311319
return response.json()["reportUrl"]
320+
321+
def getBatchDetailsIter(
322+
self,
323+
ip_addresses,
324+
batch_size=None,
325+
raise_on_fail=True,
326+
):
327+
if batch_size is None:
328+
batch_size = BATCH_MAX_SIZE
329+
330+
result = {}
331+
lookup_addresses = []
332+
for ip_address in ip_addresses:
333+
if isinstance(ip_address, IPv4Address) or isinstance(
334+
ip_address, IPv6Address
335+
):
336+
ip_address = ip_address.exploded
337+
338+
if ip_address and is_bogon(ip_address):
339+
details = {}
340+
details["ip"] = ip_address
341+
details["bogon"] = True
342+
yield Details(details)
343+
else:
344+
lookup_addresses.append(ip_address)
345+
346+
try:
347+
cached_ipaddr = self.cache[cache_key(ip_address)]
348+
result[ip_address] = cached_ipaddr
349+
except KeyError:
350+
lookup_addresses.append(ip_address)
351+
352+
# all in cache - exit early.
353+
if len(lookup_addresses) == 0:
354+
raise StopIteration(result.items())
355+
356+
url = API_URL + "/batch"
357+
headers = handler_utils.get_headers(self.access_token, self.headers)
358+
headers["content-type"] = "application/json"
359+
for i in range(0, len(lookup_addresses), batch_size):
360+
batch = lookup_addresses[i : i + batch_size]
361+
362+
try:
363+
response = requests.post(url, json=batch, headers=headers)
364+
except Exception as e:
365+
raise e
366+
367+
try:
368+
if response.status_code == 429:
369+
raise RequestQuotaExceededError()
370+
response.raise_for_status()
371+
except Exception as e:
372+
return handler_utils.return_or_fail(raise_on_fail, e)
373+
374+
details = response.json()
375+
376+
# format & cache
377+
handler_utils.format_details(
378+
details,
379+
self.countries,
380+
self.eu_countries,
381+
self.countries_flags,
382+
self.countries_currencies,
383+
self.continents,
384+
)
385+
for ip in batch:
386+
detail = details.get(ip)
387+
self.cache[cache_key(ip)] = detail
388+
yield detail

ipinfo/handler_async.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,69 @@ def _ensure_aiohttp_ready(self):
351351

352352
timeout = aiohttp.ClientTimeout(total=self.request_options["timeout"])
353353
self.httpsess = aiohttp.ClientSession(timeout=timeout)
354+
355+
async def getBatchDetailsIter(
356+
self,
357+
ip_addresses,
358+
batch_size=None,
359+
raise_on_fail=True,
360+
):
361+
if batch_size is None:
362+
batch_size = BATCH_MAX_SIZE
363+
364+
results = {}
365+
lookup_addresses = []
366+
for ip_address in ip_addresses:
367+
if isinstance(ip_address, IPv4Address) or isinstance(
368+
ip_address, IPv6Address
369+
):
370+
ip_address = ip_address.exploded
371+
372+
if ip_address and is_bogon(ip_address):
373+
details = {}
374+
details["ip"] = ip_address
375+
details["bogon"] = True
376+
yield Details(details)
377+
else:
378+
lookup_addresses.append(ip_address)
379+
380+
try:
381+
cached_ipaddr = self.cache[cache_key(ip_address)]
382+
results[ip_address] = cached_ipaddr
383+
except KeyError:
384+
lookup_addresses.append(ip_address)
385+
386+
if len(lookup_addresses) == 0:
387+
yield results.items()
388+
389+
url = API_URL + "/batch"
390+
headers = handler_utils.get_headers(self.access_token, self.headers)
391+
headers["content-type"] = "application/json"
392+
393+
async def process_batch(batch):
394+
try:
395+
async with aiohttp.ClientSession(headers=headers) as session:
396+
response = await session.post(url, json=batch)
397+
response.raise_for_status()
398+
json_response = await response.json()
399+
for ip_address, details in json_response.items():
400+
self.cache[cache_key(ip_address)] = details
401+
results[ip_address] = details
402+
except Exception as e:
403+
raise e
404+
405+
for i in range(0, len(lookup_addresses), batch_size):
406+
batch = lookup_addresses[i : i + batch_size]
407+
await process_batch(batch)
408+
409+
for ip_address, details in results.items():
410+
if isinstance(details, dict):
411+
handler_utils.format_details(
412+
details,
413+
self.countries,
414+
self.eu_countries,
415+
self.countries_flags,
416+
self.countries_currencies,
417+
self.continents,
418+
)
419+
yield ip_address, details

ipinfo/handler_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def format_details(
8080
details["country_name"] = countries.get(details.get("country"))
8181
details["isEU"] = details.get("country") in eu_countries
8282
details["country_flag_url"] = (
83-
COUNTRY_FLAGS_URL + details.get("country") + ".svg"
83+
COUNTRY_FLAGS_URL + (details.get("country") or "") + ".svg"
8484
)
8585
details["country_flag"] = copy.deepcopy(
8686
countries_flags.get(details.get("country"))

tests/handler_async_test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,27 @@ async def test_get_batch_details(batch_size):
145145
await handler.deinit()
146146

147147

148+
def _check_iterative_batch_details(ip, details, token):
149+
"""Helper for iterative batch tests."""
150+
assert ip == details.get("ip")
151+
assert "country" in details
152+
assert "city" in details
153+
if token:
154+
assert "asn" in details or "anycast" in details
155+
assert "company" in details or "org" in details
156+
assert "privacy" in details or "anycast" in details
157+
assert "abuse" in details or "anycast" in details
158+
assert "domains" in details or "anycast" in details
159+
160+
161+
@pytest.mark.parametrize("batch_size", [None, 1, 2, 3])
162+
@pytest.mark.asyncio
163+
async def test_get_iterative_batch_details(batch_size):
164+
handler, token, ips = _prepare_batch_test()
165+
async for ips, details in handler.getBatchDetailsIter(ips, batch_size):
166+
_check_iterative_batch_details(ips, details, token)
167+
168+
148169
@pytest.mark.parametrize("batch_size", [None, 1, 2, 3])
149170
@pytest.mark.asyncio
150171
async def test_get_batch_details_total_timeout(batch_size):

tests/handler_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,19 @@ def _check_batch_details(ips, details, token):
131131
assert "domains" in d
132132

133133

134+
def _check_iterative_batch_details(details, token):
135+
"""Helper for iterative batch tests."""
136+
assert "ip" in details, "Key 'ip' not found in details"
137+
assert "country" in details, "Key 'country' not found in details"
138+
assert "city" in details, "Key 'city' not found in details"
139+
if token:
140+
assert "asn" in details, "Key 'asn' not found in details"
141+
assert "company" in details, "Key 'company' not found in details"
142+
assert "privacy" in details, "Key 'privacy' not found in details"
143+
assert "abuse" in details, "Key 'abuse' not found in details"
144+
assert "domains" in details, "Key 'domains' not found in details"
145+
146+
134147
@pytest.mark.parametrize("batch_size", [None, 1, 2, 3])
135148
def test_get_batch_details(batch_size):
136149
handler, token, ips = _prepare_batch_test()
@@ -147,6 +160,14 @@ def test_get_batch_details_total_timeout(batch_size):
147160
)
148161

149162

163+
@pytest.mark.parametrize("batch_size", [None, 1, 2, 3])
164+
def test_get_iterative_batch_details(batch_size):
165+
handler, token, ips = _prepare_batch_test()
166+
details_iterator = handler.getBatchDetailsIter(ips, batch_size=batch_size)
167+
for details in details_iterator:
168+
_check_iterative_batch_details(details, token)
169+
170+
150171
#############
151172
# MAP TESTS
152173
#############
@@ -169,3 +190,10 @@ def test_bogon_details():
169190
details = handler.getDetails("127.0.0.1")
170191
assert isinstance(details, Details)
171192
assert details.all == {"bogon": True, "ip": "127.0.0.1"}
193+
194+
195+
def test_iterative_bogon_details():
196+
token = os.environ.get("IPINFO_TOKEN", "")
197+
handler = Handler(token)
198+
details = next(handler.getBatchDetailsIter(["127.0.0.1"]))
199+
assert details.all == {"bogon": True, "ip": "127.0.0.1"}

0 commit comments

Comments
 (0)