66import json
77import os
88import sys
9+ import time
910
1011import requests
1112
1213from .cache .default import DefaultCache
1314from .details import Details
14- from .exceptions import RequestQuotaExceededError
15+ from .exceptions import RequestQuotaExceededError , TimeoutExceededError
16+ from .handler_utils import (
17+ API_URL ,
18+ COUNTRY_FILE_DEFAULT ,
19+ BATCH_MAX_SIZE ,
20+ CACHE_MAXSIZE ,
21+ CACHE_TTL ,
22+ REQUEST_TIMEOUT_DEFAULT ,
23+ BATCH_REQ_TIMEOUT_DEFAULT ,
24+ )
1525from . import handler_utils
1626
1727
@@ -21,10 +31,6 @@ class Handler:
2131 Instantiates and maintains access to cache.
2232 """
2333
24- CACHE_MAXSIZE = 4096
25- CACHE_TTL = 60 * 60 * 24
26- REQUEST_TIMEOUT_DEFAULT = 2
27-
2834 def __init__ (self , access_token = None , ** kwargs ):
2935 """
3036 Initialize the Handler object with country name list and the
@@ -40,21 +46,26 @@ def __init__(self, access_token=None, **kwargs):
4046 # setup req opts
4147 self .request_options = kwargs .get ("request_options" , {})
4248 if "timeout" not in self .request_options :
43- self .request_options ["timeout" ] = self . REQUEST_TIMEOUT_DEFAULT
49+ self .request_options ["timeout" ] = REQUEST_TIMEOUT_DEFAULT
4450
4551 # setup cache
4652 if "cache" in kwargs :
4753 self .cache = kwargs ["cache" ]
4854 else :
4955 cache_options = kwargs .get ("cache_options" , {})
5056 if "maxsize" not in cache_options :
51- cache_options ["maxsize" ] = self . CACHE_MAXSIZE
57+ cache_options ["maxsize" ] = CACHE_MAXSIZE
5258 if "ttl" not in cache_options :
53- cache_options ["ttl" ] = self . CACHE_TTL
59+ cache_options ["ttl" ] = CACHE_TTL
5460 self .cache = DefaultCache (** cache_options )
5561
56- def getDetails (self , ip_address = None ):
57- """Get details for specified IP address as a Details object."""
62+ def getDetails (self , ip_address = None , timeout = None ):
63+ """
64+ Get details for specified IP address as a Details object.
65+
66+ If `timeout` is not `None`, it will override the client-level timeout
67+ just for this operation.
68+ """
5869 # If the supplied IP address uses the objects defined in the built-in
5970 # module ipaddress extract the appropriate string notation before
6071 # formatting the URL.
@@ -66,12 +77,17 @@ def getDetails(self, ip_address=None):
6677 if ip_address in self .cache :
6778 return Details (self .cache [ip_address ])
6879
80+ # prepare req http opts
81+ req_opts = {** self .request_options }
82+ if timeout is not None :
83+ req_opts ["timeout" ] = timeout
84+
6985 # not in cache; do http req
70- url = handler_utils . API_URL
86+ url = API_URL
7187 if ip_address :
7288 url += "/" + ip_address
7389 headers = handler_utils .get_headers (self .access_token )
74- response = requests .get (url , headers = headers , ** self . request_options )
90+ response = requests .get (url , headers = headers , ** req_opts )
7591 if response .status_code == 429 :
7692 raise RequestQuotaExceededError ()
7793 response .raise_for_status ()
@@ -83,7 +99,14 @@ def getDetails(self, ip_address=None):
8399
84100 return Details (details )
85101
86- def getBatchDetails (self , ip_addresses , batch_size = None ):
102+ def getBatchDetails (
103+ self ,
104+ ip_addresses ,
105+ batch_size = None ,
106+ timeout_per_batch = BATCH_REQ_TIMEOUT_DEFAULT ,
107+ timeout_total = None ,
108+ raise_on_fail = True ,
109+ ):
87110 """
88111 Get details for a batch of IP addresses at once.
89112
@@ -92,11 +115,26 @@ def getBatchDetails(self, ip_addresses, batch_size=None):
92115 all of the response data, which is at least a magnitude larger than the
93116 input list).
94117
118+ The input list is broken up into batches to abide by API requirements.
95119 The batch size can be adjusted with `batch_size` but is clipped to (and
96- also defaults to) `handler_utils.BATCH_MAX_SIZE`.
120+ also defaults to) `BATCH_MAX_SIZE`.
121+
122+ For each batch, `timeout_per_batch` indicates the maximum seconds to
123+ spend waiting for the HTTP request to complete. If any batch fails with
124+ this timeout, the whole operation fails.
125+ Defaults to `BATCH_REQ_TIMEOUT_DEFAULT` seconds.
126+
127+ `timeout_total` is a seconds-denominated hard-timeout for the time
128+ spent in HTTP operations; regardless of whether all batches have
129+ succeeded so far, if `timeout_total` is reached, the whole operation
130+ will fail. Defaults to being turned off.
131+
132+ `raise_on_fail`, if turned off, will return any result retrieved so far
133+ rather than raise an exception when errors occur, including timeout and
134+ quota errors. Defaults to on.
97135 """
98136 if batch_size == None :
99- batch_size = handler_utils . BATCH_MAX_SIZE
137+ batch_size = BATCH_MAX_SIZE
100138
101139 result = {}
102140
@@ -117,23 +155,44 @@ def getBatchDetails(self, ip_addresses, batch_size=None):
117155 else :
118156 lookup_addresses .append (ip_address )
119157
158+ # prepare req http options
159+ req_opts = {** self .request_options , "timeout" : timeout_per_batch }
160+
161+ if timeout_total is not None :
162+ start_time = time .time ()
163+
120164 # loop over batch chunks and do lookup for each.
121165 for i in range (0 , len (ip_addresses ), batch_size ):
166+ # quit if total timeout is reached.
167+ if (
168+ timeout_total is not None
169+ and time .time () - start_time > timeout_total
170+ ):
171+ if raise_on_fail :
172+ raise TimeoutExceededError ()
173+ else :
174+ return result
175+
122176 chunk = ip_addresses [i : i + batch_size ]
123177
124178 # lookup
125- url = handler_utils . API_URL + "/batch"
179+ url = API_URL + "/batch"
126180 headers = handler_utils .get_headers (self .access_token )
127181 headers ["content-type" ] = "application/json"
128182 response = requests .post (
129- url ,
130- json = lookup_addresses ,
131- headers = headers ,
132- ** self .request_options
183+ url , json = lookup_addresses , headers = headers , ** req_opts
133184 )
134- if response .status_code == 429 :
135- raise RequestQuotaExceededError ()
136- response .raise_for_status ()
185+
186+ # fail on bad status codes
187+ try :
188+ if response .status_code == 429 :
189+ raise RequestQuotaExceededError ()
190+ response .raise_for_status ()
191+ except Exception as e :
192+ if raise_on_fail :
193+ raise e
194+ else :
195+ return result
137196
138197 # fill cache
139198 json_response = response .json ()
0 commit comments