Skip to content

Commit 6ff9412

Browse files
committed
Merge pull request #41 from cp2boston/RCB-381_python_429s
Rcb 381 python 429s
2 parents 2c1ecf8 + 57ecba2 commit 6ff9412

File tree

4 files changed

+547
-429
lines changed

4 files changed

+547
-429
lines changed

docker/tox.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ commands =
1313
deps =
1414
pytest
1515
pytest-pep8
16-
httpretty==0.8.10
16+
httpretty
1717
epydoc
18-
requests
18+
requests

rosette/api.py

Lines changed: 125 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,14 @@
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

4538
try:
4639
import urlparse
47-
import urllib
4840
except ImportError:
4941
import urllib.parse as urlparse
50-
import urllib.parse as urllib
5142
try:
5243
import httplib
5344
except 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-
19772
class 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

Comments
 (0)