1+ import time
12import requests
3+ import logging
24
35try :
46 from urllib .parse import urlencode # Python 3
57except ImportError :
68 from urllib import urlencode # Python 2
79
10+ logger = logging .getLogger (__name__ )
11+ logger .addHandler (logging .NullHandler ())
12+
13+ # In seconds. Will increase each iteration
14+ RETRY_TIMEOUT_IN_CASE_OF_SERVER_ERROR = 10
15+
816
917class ConstructorError (Exception ):
10- pass
18+ def __init__ (self , message = "" ):
19+ super (ConstructorError , self ).__init__ (
20+ "Undefined error with Constructor.io: " + str (message ))
21+
22+
23+ class ConstructorInputError (ConstructorError ):
24+ def __init__ (self , message = "" ):
25+ super (Exception , self ).__init__ ("Bad request: " + str (message ))
26+
27+
28+ class ConstructorServerError (ConstructorError ):
29+ def __init__ (self , message = "" ):
30+ super (Exception , self ).__init__ ("Server error: " + str (message ))
1131
1232
1333class ConstructorIO (object ):
1434 def __init__ (self , api_token , key = None , protocol = "https" ,
15- host = "ac.cnstrc.com" , autocomplete_key = None ):
16- """
17- If you use HTTPS, you need a different version of requests
18- """
19- # Support backward capability after renaming `autocomplete_key` to `key`
35+ host = "ac.cnstrc.com" , autocomplete_key = None ,
36+ server_error_retries = 10 ):
37+
38+ # Support backward capability after
39+ # renaming `autocomplete_key` to `key`
2040 if key is None :
2141 key = autocomplete_key
2242 if key is None and autocomplete_key is None :
@@ -26,6 +46,7 @@ def __init__(self, api_token, key=None, protocol="https",
2646 self ._key = key
2747 self ._protocol = protocol
2848 self ._host = host
49+ self ._server_error_retries = server_error_retries
2950
3051 def _serialize_params (self , params , sort = False ):
3152 """
@@ -45,24 +66,42 @@ def _make_url(self, endpoint, params=None):
4566 return "{0}://{1}/{2}?{3}" .format (self ._protocol , self ._host , endpoint ,
4667 self ._serialize_params (params ))
4768
48- def query (self , query_str ):
69+ def __make_server_request (self , request_method , * args , ** kwargs ):
70+ retries_left = self ._server_error_retries
71+ timeout = RETRY_TIMEOUT_IN_CASE_OF_SERVER_ERROR
72+
73+ while True :
74+ try :
75+ # Wrap server error codes as exceptions
76+ response = request_method (* args , ** kwargs )
77+ if response .status_code // 100 == 5 :
78+ raise ConstructorServerError (response .text )
79+ elif response .status_code // 100 == 4 :
80+ raise ConstructorInputError (response .text )
81+ elif not response .status_code // 100 == 2 :
82+ raise ConstructorError (response .text )
83+ return response
84+ except ConstructorServerError as error :
85+ # Retry in case of server error
86+ if retries_left <= 0 :
87+ raise error
88+ timeout += RETRY_TIMEOUT_IN_CASE_OF_SERVER_ERROR
89+ logger .warning ('%s Retrying in %d seconds. Retries left: %d' ,
90+ error , timeout , retries_left )
91+ retries_left -= 1
92+ time .sleep (timeout )
93+
94+ def query (self , query_str , * args , ** kwargs ):
4995 url = self ._make_url ("autocomplete/" + query_str )
50- resp = requests .get (url )
51- if resp .status_code != 200 :
52- raise ConstructorError (resp .text )
53- else :
54- return resp .json ()
96+ resp = self .__make_server_request (requests .get , url , * args , ** kwargs )
97+ return resp .json ()
5598
5699 def verify (self ):
57100 url = self ._make_url ("v1/verify" )
58- resp = requests .get (
59- url ,
60- auth = (self ._api_token , "" )
61- )
62- if resp .status_code != 200 :
63- raise ConstructorError (resp .text )
64- else :
65- return resp .json ()
101+ resp = self .__make_server_request (requests .get ,
102+ url ,
103+ auth = (self ._api_token , "" ))
104+ return resp .json ()
66105
67106 def extract_params_from_kwargs (self , params , ** kwargs ):
68107 # The '_force' kwarg just indicates that `force` should be added
@@ -83,15 +122,11 @@ def add(self, item_name, autocomplete_section, **kwargs):
83122 url_params ["force" ] = 1
84123 request_method = getattr (requests , 'put' )
85124 url = self ._make_url ("v1/item" , url_params )
86- resp = request_method (
87- url ,
88- json = params ,
89- auth = (self ._api_token , "" )
90- )
91- if resp .status_code != 204 :
92- raise ConstructorError (resp .text )
93- else :
94- return True
125+ self .__make_server_request (request_method ,
126+ url ,
127+ json = params ,
128+ auth = (self ._api_token , "" ))
129+ return True
95130
96131 def add_or_update (self , item_name , autocomplete_section , ** kwargs ):
97132 if not self ._api_token :
@@ -114,15 +149,11 @@ def add_batch(self, items, autocomplete_section, **kwargs):
114149 request_method = getattr (requests , 'put' )
115150 params = {"items" : items , "autocomplete_section" : autocomplete_section }
116151 url = self ._make_url ("v1/batch_items" , url_params )
117- resp = request_method (
118- url ,
119- json = params ,
120- auth = (self ._api_token , "" )
121- )
122- if resp .status_code != 204 :
123- raise ConstructorError (resp .text )
124- else :
125- return True
152+ self .__make_server_request (request_method ,
153+ url ,
154+ json = params ,
155+ auth = (self ._api_token , "" ))
156+ return True
126157
127158 def add_or_update_batch (self , items , autocomplete_section , ** kwargs ):
128159 if not self ._api_token :
@@ -138,15 +169,11 @@ def remove(self, item_name, autocomplete_section):
138169 if not self ._api_token :
139170 raise IOError (
140171 "You must have an API token to use the Remove method!" )
141- resp = requests .delete (
142- url ,
143- json = params ,
144- auth = (self ._api_token , "" )
145- )
146- if resp .status_code != 204 :
147- raise ConstructorError (resp .text )
148- else :
149- return True
172+ self .__make_server_request (requests .delete ,
173+ url ,
174+ json = params ,
175+ auth = (self ._api_token , "" ))
176+ return True
150177
151178 def remove_batch (self , items , autocomplete_section ):
152179 if not self ._api_token :
@@ -155,15 +182,11 @@ def remove_batch(self, items, autocomplete_section):
155182 url_params = {}
156183 params = {"items" : items , "autocomplete_section" : autocomplete_section }
157184 url = self ._make_url ("v1/batch_items" , url_params )
158- resp = requests .delete (
159- url ,
160- json = params ,
161- auth = (self ._api_token , "" )
162- )
163- if resp .status_code != 204 :
164- raise ConstructorError (resp .text )
165- else :
166- return True
185+ self .__make_server_request (requests .delete ,
186+ url ,
187+ json = params ,
188+ auth = (self ._api_token , "" ))
189+ return True
167190
168191 def modify (self , item_name , autocomplete_section , ** kwargs ):
169192 params = {"item_name" : item_name ,
@@ -184,15 +207,11 @@ def modify(self, item_name, autocomplete_section, **kwargs):
184207 if not self ._api_token :
185208 raise IOError (
186209 "You must have an API token to use the Modify method!" )
187- resp = requests .put (
188- url ,
189- json = params ,
190- auth = (self ._api_token , "" )
191- )
192- if resp .status_code != 204 :
193- raise ConstructorError (resp .text )
194- else :
195- return True
210+ self .__make_server_request (requests .put ,
211+ url ,
212+ json = params ,
213+ auth = (self ._api_token , "" ))
214+ return True
196215
197216 def track_conversion (self , term , autocomplete_section , ** kwargs ):
198217 params = {
@@ -204,15 +223,11 @@ def track_conversion(self, term, autocomplete_section, **kwargs):
204223 url = self ._make_url ("v1/conversion" )
205224 if not self ._api_token :
206225 raise IOError ("You must have an API token to track conversions!" )
207- resp = requests .post (
208- url ,
209- json = params ,
210- auth = (self ._api_token , "" )
211- )
212- if resp .status_code != 204 :
213- raise ConstructorError (resp .text )
214- else :
215- return True
226+ self .__make_server_request (requests .post ,
227+ url ,
228+ json = params ,
229+ auth = (self ._api_token , "" ))
230+ return True
216231
217232 def track_click_through (self , term , autocomplete_section , ** kwargs ):
218233 params = {
@@ -227,15 +242,11 @@ def track_click_through(self, term, autocomplete_section, **kwargs):
227242 if not self ._api_token :
228243 raise IOError (
229244 "You must have an API token to track click throughs!" )
230- resp = requests .post (
231- url ,
232- json = params ,
233- auth = (self ._api_token , "" )
234- )
235- if resp .status_code != 204 :
236- raise ConstructorError (resp .text )
237- else :
238- return True
245+ self .__make_server_request (requests .post ,
246+ url ,
247+ json = params ,
248+ auth = (self ._api_token , "" ))
249+ return True
239250
240251 def track_search (self , term , ** kwargs ):
241252 params = {
@@ -246,12 +257,8 @@ def track_search(self, term, **kwargs):
246257 url = self ._make_url ("v1/search" )
247258 if not self ._api_token :
248259 raise IOError ("You must have an API token to track searches!" )
249- resp = requests .post (
250- url ,
251- json = params ,
252- auth = (self ._api_token , "" )
253- )
254- if resp .status_code != 204 :
255- raise ConstructorError (resp .text )
256- else :
257- return True
260+ self .__make_server_request (requests .post ,
261+ url ,
262+ json = params ,
263+ auth = (self ._api_token , "" ))
264+ return True
0 commit comments