33import singer
44from singer import metrics , utils
55from requests .auth import HTTPBasicAuth
6+ from requests .exceptions import ConnectionError , ChunkedEncodingError
7+ import http .client
8+ from requests .exceptions import Timeout
69
710LOGGER = singer .get_logger ()
8- API_VERSION = '2017-04-01'
11+ API_VERSION = "2017-04-01"
12+ REQUEST_TIMEOUT = 300
913
1014
1115class Server5xxError (Exception ):
@@ -103,7 +107,7 @@ def get_exception_for_error_code(error_code):
103107
104108
105109def raise_for_error (response ):
106- LOGGER .error (' ERROR {}: {}, REASON: {}' .format (response .status_code ,
110+ LOGGER .error (" ERROR {}: {}, REASON: {}" .format (response .status_code ,
107111 response .text , response .reason ))
108112 try :
109113 response .raise_for_status ()
@@ -115,10 +119,10 @@ def raise_for_error(response):
115119 # us a 2xx response nor a response content.
116120 return
117121 response = response .json ()
118- if (' error' in response ) or (' errorCode' in response ):
119- message = ' %s: %s' % (response .get (' error' , str (error )),
120- response .get (' message' , ' Unknown Error' ))
121- error_code = response .get (' code' )
122+ if (" error" in response ) or (" errorCode" in response ):
123+ message = " %s: %s" % (response .get (" error" , str (error )),
124+ response .get (" message" , " Unknown Error" ))
125+ error_code = response .get (" code" )
122126 ex = get_exception_for_error_code (error_code )
123127 raise ex (message )
124128 raise MailshakeError (error )
@@ -134,12 +138,15 @@ class MailshakeClient:
134138
135139 def __init__ (self ,
136140 api_key ,
137- user_agent = None ):
141+ user_agent = None ,
142+ request_timeout = REQUEST_TIMEOUT ):
138143 self .__api_key = api_key
139144 self .base_url = "https://api.mailshake.com/{}" .format (
140145 API_VERSION )
141146 self .__user_agent = user_agent
142147 self .__session = requests .Session ()
148+ self .__verified = False
149+ self .request_timeout = request_timeout or REQUEST_TIMEOUT
143150
144151 def __enter__ (self ):
145152 self .check_access ()
@@ -155,25 +162,31 @@ def __exit__(self, exception_type, exception_value, traceback):
155162 @utils .ratelimit (1 , 1.2 )
156163 def check_access (self ):
157164 if self .__api_key is None :
158- raise Exception (' Error: Missing api_key in tap_config.json.' )
165+ raise Exception (" Error: Missing api_key in tap_config.json." )
159166 headers = {}
160- endpoint = 'me'
161- url = ' {}/{}' .format (self .base_url , endpoint )
167+ endpoint = "me"
168+ url = " {}/{}" .format (self .base_url , endpoint )
162169 if self .__user_agent :
163- headers [' User-Agent' ] = self .__user_agent
164- headers [' Accept' ] = ' application/json'
170+ headers [" User-Agent" ] = self .__user_agent
171+ headers [" Accept" ] = " application/json"
165172 response = self .__session .get (
166173 url = url ,
167174 headers = headers ,
168- auth = HTTPBasicAuth (self .__api_key , '' ))
175+ auth = HTTPBasicAuth (self .__api_key , "" ))
169176 if response .status_code != 200 :
170- LOGGER .error (' Error status_code = {}' .format (response .status_code ))
177+ LOGGER .error (" Error status_code = {}" .format (response .status_code ))
171178 return False
172179 return True
173180
174- @backoff .on_exception (backoff .expo ,
175- (Server5xxError , ConnectionError , MailshakeAPILimitReachedError ),
176- factor = 3 )
181+ @backoff .on_exception (
182+ backoff .expo ,
183+ (Server5xxError , ConnectionError , ChunkedEncodingError , http .client .IncompleteRead ,
184+ MailshakeAPILimitReachedError , Timeout ),
185+ factor = 3 ,
186+ max_tries = 5 ,
187+ on_backoff = lambda details : LOGGER .warning (
188+ f"Retrying { details ['target' ].__name__ } after: { details ['exception' ]} " )
189+ )
177190 @utils .ratelimit (1 , 3 )
178191 def request (self , method , path = None , url = None , json = None , ** kwargs ):
179192 """Perform HTTP request"""
@@ -185,50 +198,55 @@ def request(self, method, path=None, url=None, json=None, **kwargs):
185198 # self.__verified = self.check_access()
186199
187200 if not url and path :
188- url = ' {}/{}' .format (self .base_url , path )
201+ url = " {}/{}" .format (self .base_url , path )
189202
190- if ' endpoint' in kwargs :
191- endpoint = kwargs [' endpoint' ]
192- del kwargs [' endpoint' ]
203+ if " endpoint" in kwargs :
204+ endpoint = kwargs [" endpoint" ]
205+ del kwargs [" endpoint" ]
193206 else :
194207 endpoint = None
195208
196- if ' headers' not in kwargs :
197- kwargs [' headers' ] = {}
209+ if " headers" not in kwargs :
210+ kwargs [" headers" ] = {}
198211
199- kwargs [' headers' ][ ' Accept' ] = ' application/json'
212+ kwargs [" headers" ][ " Accept" ] = " application/json"
200213
201214 if self .__user_agent :
202- kwargs [' headers' ][ ' User-Agent' ] = self .__user_agent
215+ kwargs [" headers" ][ " User-Agent" ] = self .__user_agent
203216
204- if method == ' POST' :
205- kwargs [' headers' ][ ' Content-Type' ] = ' application/json'
217+ if method == " POST" :
218+ kwargs [" headers" ][ " Content-Type" ] = " application/json"
206219
207220 with metrics .http_request_timer (endpoint ) as timer :
221+ request_timeout = kwargs .pop ("request_timeout" , self .request_timeout )
208222 response = self .__session .request (
209223 method = method ,
210224 url = url ,
211225 json = json ,
212- auth = HTTPBasicAuth (self .__api_key , '' ),
226+ auth = HTTPBasicAuth (self .__api_key , "" ),
227+ timeout = request_timeout ,
213228 ** kwargs )
214229 timer .tags [metrics .Tag .http_status_code ] = response .status_code
215230
216231 if response .status_code >= 500 :
217232 raise Server5xxError ()
218233
234+ if response .status_code == 429 :
235+ raise MailshakeAPILimitReachedError ("Rate limit exceeded" )
236+
219237 if response .status_code != 200 :
220238 raise_for_error (response )
221239
222240 # pagination details (nextToken) are returned in the body
223241 next_token = None
224242 response_body = response .json ()
225- if response_body .get (' nextToken' ) != "" :
226- next_token = response_body .get (' nextToken' )
243+ if response_body .get (" nextToken" ) != "" :
244+ next_token = response_body .get (" nextToken" )
227245
228246 return response_body , next_token
229247
230248 def get (self , path , ** kwargs ):
231- return self .request (' GET' , path = path , ** kwargs )
249+ return self .request (" GET" , path = path , ** kwargs )
232250
233251 def post (self , path , ** kwargs ):
234- return self .request (' POST' , path = path , ** kwargs )
252+ return self .request (" POST" , path = path , ** kwargs )
0 commit comments