3131
3232_BINDING_VERSION = "1.0"
3333_GZIP_BYTEARRAY = bytearray ([0x1F , 0x8b , 0x08 ])
34- N_RETRIES = 3
35- HTTP_CONNECTION = None
36- REUSE_CONNECTION = True
37- CONNECTION_TYPE = ""
38- CONNECTION_START = datetime .now ()
39- CONNECTION_REFRESH_DURATION = 86400
40- N_RETRIES = 3
4134
4235_IsPy3 = sys .version_info [0 ] == 3
4336
4437
4538try :
4639 import urlparse
47- import urllib
4840except ImportError :
4941 import urllib .parse as urlparse
50- import urllib .parse as urllib
5142try :
5243 import httplib
5344except ImportError :
@@ -78,122 +69,6 @@ def _my_loads(obj, response_headers):
7869 d2 .update (response_headers )
7970 return d2
8071
81-
82- def _retrying_request (op , url , data , headers ):
83- global HTTP_CONNECTION
84- global REUSE_CONNECTION
85- global CONNECTION_TYPE
86- global CONNECTION_START
87- global CONNECTION_REFRESH_DURATION
88-
89- headers ['User-Agent' ] = "RosetteAPIPython/" + _BINDING_VERSION
90- timeDelta = datetime .now () - CONNECTION_START
91- totalTime = timeDelta .days * 86400 + timeDelta .seconds
92-
93- parsed = urlparse .urlparse (url )
94- if parsed .scheme != CONNECTION_TYPE :
95- totalTime = CONNECTION_REFRESH_DURATION
96-
97- if not REUSE_CONNECTION or HTTP_CONNECTION is None or totalTime >= CONNECTION_REFRESH_DURATION :
98- parsed = urlparse .urlparse (url )
99- loc = parsed .netloc
100- CONNECTION_TYPE = parsed .scheme
101- CONNECTION_START = datetime .now ()
102- if parsed .scheme == "https" :
103- HTTP_CONNECTION = httplib .HTTPSConnection (loc )
104- else :
105- HTTP_CONNECTION = httplib .HTTPConnection (loc )
106-
107- message = None
108- code = "unknownError"
109- rdata = None
110- response_headers = {}
111- for i in range (N_RETRIES + 1 ):
112- # Try to connect with the Rosette API server
113- # 500 errors will store a message and code
114- try :
115- HTTP_CONNECTION .request (op , url , data , headers )
116- response = HTTP_CONNECTION .getresponse ()
117- status = response .status
118- rdata = response .read ()
119- response_headers ["responseHeaders" ] = (dict (response .getheaders ()))
120- if status < 500 :
121- if not REUSE_CONNECTION :
122- HTTP_CONNECTION .close ()
123- return rdata , status , response_headers
124- if rdata is not None :
125- try :
126- the_json = _my_loads (rdata , response_headers )
127- if "message" in the_json :
128- message = the_json ["message" ]
129- if "code" in the_json :
130- code = the_json ["code" ]
131- except :
132- pass
133- # If there are issues connecting to the API server,
134- # try to regenerate the connection as long as there are
135- # still retries left.
136- # A short sleep delay occurs (similar to google reconnect)
137- # if the problem was a temporal one.
138- except (httplib .BadStatusLine , gaierror ) as e :
139- totalTime = CONNECTION_REFRESH_DURATION
140- if i == N_RETRIES - 1 :
141- raise RosetteException ("ConnectionError" , "Unable to establish connection to the Rosette API server" , url )
142- else :
143- if not REUSE_CONNECTION or HTTP_CONNECTION is None or totalTime >= CONNECTION_REFRESH_DURATION :
144- time .sleep (min (5 * (i + 1 ) * (i + 1 ), 300 ))
145- parsed = urlparse .urlparse (url )
146- loc = parsed .netloc
147- CONNECTION_TYPE = parsed .scheme
148- CONNECTION_START = datetime .now ()
149- if parsed .scheme == "https" :
150- HTTP_CONNECTION = httplib .HTTPSConnection (loc )
151- else :
152- HTTP_CONNECTION = httplib .HTTPConnection (loc )
153-
154- # Do not wait to retry -- the model is that a bunch of dynamically-routed
155- # resources has failed -- Retry means some other set of servelets and their
156- # underlings will be called up, and maybe they'll do better.
157- # This will not help with a persistent or impassible delay situation,
158- # but the former case is thought to be more likely.
159-
160- if not REUSE_CONNECTION :
161- HTTP_CONNECTION .close ()
162-
163- if message is None :
164- message = "A retryable network operation has not succeeded after " + str (N_RETRIES ) + " attempts"
165-
166- raise RosetteException (code , message , url )
167-
168-
169- def _get_http (url , headers ):
170- (rdata , status , response_headers ) = _retrying_request ("GET" , url , None , headers )
171- return _ReturnObject (_my_loads (rdata , response_headers ), status )
172-
173-
174- def _post_http (url , data , headers ):
175- if data is None :
176- json_data = ""
177- else :
178- json_data = json .dumps (data )
179-
180- (rdata , status , response_headers ) = _retrying_request ("POST" , url , json_data , headers )
181-
182- if len (rdata ) > 3 and rdata [0 :3 ] == _GZIP_SIGNATURE :
183- buf = BytesIO (rdata )
184- rdata = gzip .GzipFile (fileobj = buf ).read ()
185-
186- return _ReturnObject (_my_loads (rdata , response_headers ), status )
187-
188-
189- def add_query (orig_url , key , value ):
190- parts = urlparse .urlsplit (orig_url )
191- queries = urlparse .parse_qsl (parts [3 ])
192- queries .append ((key , value ))
193- qs = urllib .urlencode (queries )
194- return urlparse .urlunsplit ((parts [0 ], parts [1 ], parts [2 ], qs , parts [4 ]))
195-
196-
19772class RosetteException (Exception ):
19873 """Exception thrown by all Rosette API operations for errors local and remote.
19974
@@ -439,6 +314,7 @@ def __init__(self, api, suburl):
439314 self .checker = lambda : api .check_version ()
440315 self .suburl = suburl
441316 self .debug = api .debug
317+ self .api = api
442318
443319 def __finish_result (self , r , ename ):
444320 code = r .status_code
@@ -455,40 +331,36 @@ def __finish_result(self, r, ename):
455331 else :
456332 complaint_url = ename + " " + self .suburl
457333
458- if "code" in the_json :
459- server_code = the_json ["code" ]
460- else :
461- server_code = "unknownError"
462-
463- raise RosetteException (server_code ,
334+ raise RosetteException (code ,
464335 complaint_url + " : failed to communicate with Rosette" ,
465336 msg )
466337
338+
467339 def info (self ):
468340 """Issues an "info" request to the L{EndpointCaller}'s specific endpoint.
469341 @return: A dictionary telling server version and other
470342 identifying data."""
471343 url = self .service_url + "info"
472344 if self .debug :
473- url = add_query ( url , "debug" , " true" )
345+ headers [ 'X-RosetteAPI-Devel' ] = ' true'
474346 self .logger .info ('info: ' + url )
475347 headers = {'Accept' : 'application/json' }
476348 if self .user_key is not None :
477349 headers ["X-RosetteAPI-Key" ] = self .user_key
478- r = _get_http (url , headers = headers )
350+ r = self . api . _get_http (url , headers = headers )
479351 return self .__finish_result (r , "info" )
480352
481353 def checkVersion (self ):
482354 """Issues a special "info" request to the L{EndpointCaller}'s specific endpoint.
483355 @return: A dictionary containing server version as well as version check"""
484356 url = self .service_url + "info?clientVersion=" + _BINDING_VERSION
485357 if self .debug :
486- url = add_query ( url , "debug" , " true" )
358+ headers [ "X-RosetteAPI-Devel" ] = ' true'
487359 self .logger .info ('info: ' + url )
488360 headers = {'Accept' : 'application/json' }
489361 if self .user_key is not None :
490362 headers ["X-RosetteAPI-Key" ] = self .user_key
491- r = _post_http (url , None , headers = headers )
363+ r = self . api . _post_http (url , None , headers )
492364 return self .__finish_result (r , "info" )
493365
494366 def ping (self ):
@@ -499,12 +371,12 @@ def ping(self):
499371
500372 url = self .service_url + 'ping'
501373 if self .debug :
502- url = add_query ( url , "debug" , " true" )
374+ headers [ 'X-RosetteAPI-Devel' ] = ' true'
503375 self .logger .info ('Ping: ' + url )
504376 headers = {'Accept' : 'application/json' }
505377 if self .user_key is not None :
506378 headers ["X-RosetteAPI-Key" ] = self .user_key
507- r = _get_http (url , headers = headers )
379+ r = self . api . _get_http (url , headers = headers )
508380 return self .__finish_result (r , "ping" )
509381
510382 def call (self , parameters ):
@@ -558,12 +430,12 @@ def call(self, parameters):
558430 r = _ReturnObject (_my_loads (rdata , response_headers ), status )
559431 else :
560432 if self .debug :
561- url = add_query ( url , "debug" , "true" )
433+ headers [ 'X-RosetteAPI-Devel' ] = True
562434 self .logger .info ('operate: ' + url )
563435 headers ['Accept' ] = "application/json"
564436 headers ['Accept-Encoding' ] = "gzip"
565437 headers ['Content-Type' ] = "application/json"
566- r = _post_http (url , params_to_serialize , headers )
438+ r = self . api . _post_http (url , params_to_serialize , headers )
567439 return self .__finish_result (r , "operate" )
568440
569441
@@ -573,7 +445,8 @@ class API:
573445 Call instance methods upon this object to obtain L{EndpointCaller} objects
574446 which can communicate with particular Rosette server endpoints.
575447 """
576- def __init__ (self , user_key = None , service_url = 'https://api.rosette.com/rest/v1/' , retries = 3 , reuse_connection = True , refresh_duration = 86400 , debug = False ):
448+
449+ def __init__ (self , user_key = None , service_url = 'https://api.rosette.com/rest/v1/' , retries = 5 , reuse_connection = True , refresh_duration = 0.5 , debug = False ):
577450 """ Create an L{API} object.
578451 @param user_key: (Optional; required for servers requiring authentication.) An authentication string to be sent
579452 as user_key with all requests. The default Rosette server requires authentication.
@@ -587,27 +460,126 @@ def __init__(self, user_key=None, service_url='https://api.rosette.com/rest/v1/'
587460 self .debug = debug
588461 self .version_checked = False
589462
590- global N_RETRIES
591- global REUSE_CONNECTION
592- global CONNECTION_REFRESH_DURATION
593-
594463 if (retries < 1 ):
595464 retries = 1
596- if (refresh_duration < 60 ):
597- refresh_duration = 60
598- N_RETRIES = retries
599- REUSE_CONNECTION = reuse_connection
600- CONNECTION_REFRESH_DURATION = refresh_duration
465+ if (refresh_duration < 0 ):
466+ refresh_duration = 0
467+
468+ self .num_retries = retries
469+ self .reuse_connection = reuse_connection
470+ self .connection_refresh_duration = refresh_duration
471+ self .http_connection = None
472+
473+ def _connect (self , parsedUrl ):
474+ """ Simple connection method
475+ @param parsedUrl: The URL on which to process
476+ """
477+ if not self .reuse_connection or self .http_connection is None :
478+ loc = parsedUrl .netloc
479+ if parsedUrl .scheme == "https" :
480+ self .http_connection = httplib .HTTPSConnection (loc )
481+ else :
482+ self .http_connection = httplib .HTTPConnection (loc )
483+
484+
485+ def _make_request (self , op , url , data , headers ):
486+ """
487+ Handles the actual request, retrying if a 429 is encountered
488+
489+ @param op: POST or GET
490+ @param url: endpoing URL
491+ @param data: request data
492+ @param headers: request headers
493+ """
494+ headers ['User-Agent' ] = "RosetteAPIPython/" + _BINDING_VERSION
495+ parsedUrl = urlparse .urlparse (url )
496+
497+ self ._connect (parsedUrl )
498+
499+ message = None
500+ code = "unknownError"
501+ rdata = None
502+ response_headers = {}
503+ for i in range (self .num_retries + 1 ):
504+ try :
505+ self .http_connection .request (op , url , data , headers )
506+ response = self .http_connection .getresponse ()
507+ status = response .status
508+ rdata = response .read ()
509+ response_headers ["responseHeaders" ] = (dict (response .getheaders ()))
510+ if status == 200 :
511+ if not self .reuse_connection :
512+ self .http_connection .close ()
513+ return rdata , status , response_headers
514+ if status == 429 :
515+ code = status
516+ message = "{0} ({1})" .format (rdata , i )
517+ time .sleep (self .connection_refresh_duration )
518+ self .http_connection .close ()
519+ self ._connect (parsedUrl )
520+ continue ;
521+ if rdata is not None :
522+ try :
523+ the_json = _my_loads (rdata , response_headers )
524+ if 'message' in the_json :
525+ message = the_json ['message' ]
526+ if "code" in the_json :
527+ code = the_json ['code' ]
528+ else :
529+ code = status
530+ raise RosetteException (code , message , url )
531+ except :
532+ raise
533+ except (httplib .BadStatusLine , gaierror ) as e :
534+ raise RosetteException ("ConnectionError" , "Unable to establish connection to the Rosette API server" , url )
535+
536+ if not self .reuse_connection :
537+ self .http_connection .close ()
538+
539+ raise RosetteException (code , message , url )
540+
541+ def _get_http (self , url , headers ):
542+ """
543+ Simple wrapper for the GET request
544+
545+ @param url: endpoint URL
546+ @param headers: request headers
547+ """
548+ (rdata , status , response_headers ) = self ._make_request ("GET" , url , None , headers )
549+ return _ReturnObject (_my_loads (rdata , response_headers ), status )
550+
551+
552+ def _post_http (self , url , data , headers ):
553+ """
554+ Simple wrapper for the POST request
555+
556+ @param url: endpoint URL
557+ @param data: request data
558+ @param headers: request headers
559+ """
560+ if data is None :
561+ json_data = ""
562+ else :
563+ json_data = json .dumps (data )
564+
565+ (rdata , status , response_headers ) = self ._make_request ("POST" , url , json_data , headers )
566+
567+ if len (rdata ) > 3 and rdata [0 :3 ] == _GZIP_SIGNATURE :
568+ buf = BytesIO (rdata )
569+ rdata = gzip .GzipFile (fileobj = buf ).read ()
570+
571+ return _ReturnObject (_my_loads (rdata , response_headers ), status )
601572
602573 def check_version (self ):
574+ """
575+ Info call to check binding version against the current Rosette API
576+ """
603577 if self .version_checked :
604578 return True
605579 op = EndpointCaller (self , None )
606580 result = op .checkVersion ()
607- version = "." .join (result ["version" ].split ("." )[0 :2 ])
608- if result ['versionChecked' ] is False :
609- raise RosetteException ("incompatibleVersion" , "The server version is not compatible with binding version " + _BINDING_VERSION ,
610- version )
581+ if 'versionChecked' not in result or result ['versionChecked' ] is False :
582+ raise RosetteException ("incompatibleVersion" , "The server version is not compatible with binding version " + _BINDING_VERSION , '' )
611583 self .version_checked = True
612584 return True
613585
0 commit comments