Skip to content

Commit 4957643

Browse files
improved error handling
1 parent eadea2b commit 4957643

File tree

1 file changed

+86
-32
lines changed

1 file changed

+86
-32
lines changed

cxone_api/client.py

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
import uuid
44
import urllib
55
import requests
6-
from .__version__ import __version__ as cxone_api_version
7-
from .exceptions import AuthException, CommunicationException
6+
import logging
7+
import random
8+
from requests.exceptions import ProxyError, HTTPError, ConnectionError, ReadTimeout, ConnectTimeout
9+
from cxone_api.__version__ import __version__ as cxone_api_version
10+
from cxone_api.exceptions import AuthException, CommunicationException
811

912

1013
class CxOneClient:
1114
__AGENT_NAME = 'CxOne PyClient'
1215

1316
def __common__init(self, agent_name, tenant_auth_endpoint,
14-
api_endpoint, timeout, retries, proxy, ssl_verify):
17+
api_endpoint, timeout, retries, retry_delay_s, randomize_retry_delay, proxy, ssl_verify):
1518

1619
self.__version = cxone_api_version
1720
self.__agent = f"{agent_name}/({CxOneClient.__AGENT_NAME}/{self.__version})"
@@ -24,16 +27,19 @@ def __common__init(self, agent_name, tenant_auth_endpoint,
2427
self.__api_endpoint = api_endpoint
2528
self.__timeout = timeout
2629
self.__retries = retries
30+
self.__retry_delay = retry_delay_s
31+
self.__randomize_retry_delay = randomize_retry_delay
2732

2833
self.__auth_result = None
2934

3035

3136
@staticmethod
3237
def create_with_oauth(oauth_id, oauth_secret, agent_name, tenant_auth_endpoint,
33-
api_endpoint, timeout=60, retries=3, proxy=None, ssl_verify=True):
38+
api_endpoint, timeout=60, retries=3, retry_delay_s=15, randomize_retry_delay=True,
39+
proxy=None, ssl_verify=True):
3440
inst = CxOneClient()
3541
inst.__common__init(agent_name, tenant_auth_endpoint, api_endpoint, timeout,
36-
retries, proxy, ssl_verify)
42+
retries, retry_delay_s, randomize_retry_delay, proxy, ssl_verify)
3743

3844
inst.__auth_content = urllib.parse.urlencode( {
3945
"grant_type" : "client_credentials",
@@ -45,10 +51,12 @@ def create_with_oauth(oauth_id, oauth_secret, agent_name, tenant_auth_endpoint,
4551

4652
@staticmethod
4753
def create_with_api_key(api_key, agent_name, tenant_auth_endpoint,
48-
api_endpoint, timeout=60, retries=3, proxy=None, ssl_verify=True):
54+
api_endpoint, timeout=60, retries=3, retry_delay_s=15, randomize_retry_delay=True,
55+
proxy=None, ssl_verify=True):
4956
inst = CxOneClient()
5057
inst.__common__init(agent_name, tenant_auth_endpoint,
51-
api_endpoint, timeout, retries, proxy, ssl_verify)
58+
api_endpoint, timeout, retries, retry_delay_s,
59+
randomize_retry_delay, proxy, ssl_verify)
5260

5361
inst.__auth_content = urllib.parse.urlencode( {
5462
"grant_type" : "refresh_token",
@@ -78,24 +86,61 @@ async def __get_request_headers(self):
7886
if self.__auth_result is None:
7987
await self.__do_auth()
8088

89+
if self.__auth_result is None:
90+
return None
91+
8192
return {
8293
"Authorization" : f"Bearer {self.__auth_result['access_token']}",
8394
"Accept" : "*/*; version=1.0",
8495
"User-Agent" : self.__agent,
8596
"CorrelationId" : self.__corelation_id
8697
}
8798

99+
async def __delay(self):
100+
if self.__retry_delay > 0:
101+
delay = random.randint(1, self.__retry_delay) if self.__randomize_retry_delay else self.__retry_delay
102+
await asyncio.sleep(delay)
103+
104+
async def __should_continue_retry(self, response : requests.Response, log : logging.Logger, try_attempt : int, exception : BaseException = None) -> bool:
105+
if exception is not None:
106+
log.exception(exception)
107+
108+
if (response is not None and response.status_code in [500, 502, 503]) or exception is not None:
109+
if try_attempt < self.__retries - 1:
110+
msg = ""
111+
112+
if response is not None:
113+
msg = f" after response error {response.status_code} for {response.request.method} {response.url}"
114+
elif exception is not None:
115+
msg = f" after exception {type(exception).__name__}."
116+
117+
log.warning(f"Delaying before retry{msg}")
118+
await self.__delay()
119+
return True
120+
121+
return False
122+
88123
async def __auth_task(self):
89-
response = None
90-
for _ in range(0, self.__retries):
91-
response = await asyncio.to_thread(requests.post, self.auth_endpoint,
92-
data=self.__auth_content, timeout=self.__timeout,
93-
proxies=self.__proxy, verify=self.__ssl_verify, headers={
94-
"Content-Type" : "application/x-www-form-urlencoded",
95-
"Accept" : "application/json"
96-
})
97-
if response.ok:
98-
return response.json()
124+
_log = logging.getLogger("CxOneClient.__auth_task")
125+
126+
for attempt in range(0, self.__retries):
127+
response = None
128+
try:
129+
response = await asyncio.to_thread(requests.post, self.auth_endpoint,
130+
data=self.__auth_content, timeout=self.__timeout,
131+
proxies=self.__proxy, verify=self.__ssl_verify, headers={
132+
"Content-Type" : "application/x-www-form-urlencoded",
133+
"Accept" : "application/json"
134+
})
135+
except (ProxyError, HTTPError, ConnectionError, ReadTimeout, ConnectTimeout) as ex:
136+
if not await self.__should_continue_retry(response, _log, attempt, ex):
137+
raise
138+
else:
139+
if response.ok:
140+
return response.json()
141+
142+
if not await self.__should_continue_retry(response, _log, attempt):
143+
break
99144

100145
raise AuthException("CheckmarxOne response: "
101146
f"{response.reason if not response is None else 'Unknown error'}")
@@ -113,26 +158,35 @@ async def __do_auth(self):
113158
self.__auth_result = await self.__auth_task()
114159

115160
async def exec_request(self, verb_func, *args, **kwargs):
161+
_log = logging.getLogger("CxOneClient.exec_request")
162+
116163
if not self.__proxy is None:
117164
kwargs['proxies'] = self.__proxy
118165

119166
kwargs['verify'] = self.__ssl_verify
120-
121-
for _ in range(0, self.__retries):
122-
auth_headers = await self.__get_request_headers()
123-
124-
if 'headers' in kwargs.keys():
125-
for h in auth_headers.keys():
126-
kwargs['headers'][h] = auth_headers[h]
167+
kwargs['timeout'] = self.__timeout
168+
169+
for attempt in range(0, self.__retries):
170+
response = None
171+
try:
172+
auth_headers = await self.__get_request_headers()
173+
174+
if 'headers' in kwargs.keys():
175+
for h in auth_headers.keys():
176+
kwargs['headers'][h] = auth_headers[h]
177+
else:
178+
kwargs['headers'] = auth_headers
179+
180+
response = await asyncio.to_thread(verb_func, *args, **kwargs)
181+
except (ProxyError, HTTPError, ConnectionError, ReadTimeout, ConnectTimeout) as ex:
182+
if not await self.__should_continue_retry(response, _log, attempt, ex):
183+
raise
127184
else:
128-
kwargs['headers'] = auth_headers
129-
130-
response = await asyncio.to_thread(verb_func, *args, **kwargs)
185+
if response.status_code == 401:
186+
await self.__do_auth()
187+
continue
131188

132-
if response.status_code == 401:
133-
await self.__do_auth()
134-
else:
135-
return response
189+
if response.ok or not await self.__should_continue_retry(response, _log, attempt):
190+
return response
136191

137192
raise CommunicationException(verb_func, *args, **kwargs)
138-

0 commit comments

Comments
 (0)