22import re
33import io
44import json
5+ import time
56import logging
67import collections
78import platform
2627 from json import JSONDecodeError as RequestsJSONDecodeError
2728
2829from .constants import (
30+ SERVER_TIMEOUT_ENV_KEY ,
31+ SERVER_RETRIES_ENV_KEY ,
2932 DEFAULT_PRODUCT_TYPE_FIELDS ,
3033 DEFAULT_PROJECT_FIELDS ,
3134 DEFAULT_FOLDER_FIELDS ,
@@ -127,6 +130,8 @@ def __init__(self, response, data=None):
127130
128131 @property
129132 def text (self ):
133+ if self ._response is None :
134+ return self .detail
130135 return self ._response .text
131136
132137 @property
@@ -135,6 +140,8 @@ def orig_response(self):
135140
136141 @property
137142 def headers (self ):
143+ if self ._response is None :
144+ return {}
138145 return self ._response .headers
139146
140147 @property
@@ -148,6 +155,8 @@ def data(self):
148155
149156 @property
150157 def content (self ):
158+ if self ._response is None :
159+ return b""
151160 return self ._response .content
152161
153162 @property
@@ -339,7 +348,11 @@ class ServerAPI(object):
339348 variable value 'AYON_CERT_FILE' by default.
340349 create_session (Optional[bool]): Create session for connection if
341350 token is available. Default is True.
351+ timeout (Optional[float]): Timeout for requests.
352+ max_retries (Optional[int]): Number of retries for requests.
342353 """
354+ _default_timeout = 10.0
355+ _default_max_retries = 3
343356
344357 def __init__ (
345358 self ,
@@ -352,6 +365,8 @@ def __init__(
352365 ssl_verify = None ,
353366 cert = None ,
354367 create_session = True ,
368+ timeout = None ,
369+ max_retries = None ,
355370 ):
356371 if not base_url :
357372 raise ValueError ("Invalid server URL {}" .format (str (base_url )))
@@ -370,6 +385,9 @@ def __init__(
370385 )
371386 self ._sender = sender
372387
388+ self ._timeout = timeout
389+ self ._max_retries = max_retries
390+
373391 if ssl_verify is None :
374392 # Custom AYON env variable for CA file or 'True'
375393 # - that should cover most default behaviors in 'requests'
@@ -474,6 +492,87 @@ def set_cert(self, cert):
474492 ssl_verify = property (get_ssl_verify , set_ssl_verify )
475493 cert = property (get_cert , set_cert )
476494
495+ @classmethod
496+ def get_default_timeout (cls ):
497+ """Default value for requests timeout.
498+
499+ First looks for environment variable SERVER_TIMEOUT_ENV_KEY which
500+ can affect timeout value. If not available then use class
501+ attribute '_default_timeout'.
502+
503+ Returns:
504+ float: Timeout value in seconds.
505+ """
506+
507+ try :
508+ return float (os .environ .get (SERVER_TIMEOUT_ENV_KEY ))
509+ except (ValueError , TypeError ):
510+ pass
511+
512+ return cls ._default_timeout
513+
514+ @classmethod
515+ def get_default_max_retries (cls ):
516+ """Default value for requests max retries.
517+
518+ First looks for environment variable SERVER_RETRIES_ENV_KEY, which
519+ can affect max retries value. If not available then use class
520+ attribute '_default_max_retries'.
521+
522+ Returns:
523+ int: Max retries value.
524+ """
525+
526+ try :
527+ return int (os .environ .get (SERVER_RETRIES_ENV_KEY ))
528+ except (ValueError , TypeError ):
529+ pass
530+
531+ return cls ._default_max_retries
532+
533+ def get_timeout (self ):
534+ """Current value for requests timeout.
535+
536+ Returns:
537+ float: Timeout value in seconds.
538+ """
539+
540+ return self ._timeout
541+
542+ def set_timeout (self , timeout ):
543+ """Change timeout value for requests.
544+
545+ Args:
546+ timeout (Union[float, None]): Timeout value in seconds.
547+ """
548+
549+ if timeout is None :
550+ timeout = self .get_default_timeout ()
551+ self ._timeout = float (timeout )
552+
553+ def get_max_retries (self ):
554+ """Current value for requests max retries.
555+
556+ Returns:
557+ int: Max retries value.
558+ """
559+
560+ return self ._max_retries
561+
562+ def set_max_retries (self , max_retries ):
563+ """Change max retries value for requests.
564+
565+ Args:
566+ max_retries (Union[int, None]): Max retries value.
567+ """
568+
569+ if max_retries is None :
570+ max_retries = self .get_default_max_retries ()
571+ self ._max_retries = int (max_retries )
572+
573+ timeout = property (get_timeout , set_timeout )
574+ max_retries = property (get_max_retries , set_max_retries )
575+
477576 @property
478577 def access_token (self ):
479578 """Access token used for authorization to server.
@@ -1004,6 +1103,10 @@ def _logout(self):
10041103 logout_from_server (self ._base_url , self ._access_token )
10051104
10061105 def _do_rest_request (self , function , url , ** kwargs ):
1106+ kwargs .setdefault ("timeout" , self .timeout )
1107+ max_retries = kwargs .get ("max_retries" , self .max_retries )
1108+ if max_retries < 1 :
1109+ max_retries = 1
10071110 if self ._session is None :
10081111 # Validate token if was not yet validated
10091112 # - ignore validation if we're in middle of
@@ -1023,38 +1126,54 @@ def _do_rest_request(self, function, url, **kwargs):
10231126 elif isinstance (function , RequestType ):
10241127 function = self ._session_functions_mapping [function ]
10251128
1026- try :
1027- response = function (url , ** kwargs )
1129+ response = None
1130+ new_response = None
1131+ for _ in range (max_retries ):
1132+ try :
1133+ response = function (url , ** kwargs )
1134+ break
1135+
1136+ except ConnectionRefusedError :
1137+ # Server may be restarting
1138+ new_response = RestApiResponse (
1139+ None ,
1140+ {"detail" : "Unable to connect the server. Connection refused" }
1141+ )
1142+ except requests .exceptions .Timeout :
1143+ # Connection timed out
1144+ new_response = RestApiResponse (
1145+ None ,
1146+ {"detail" : "Connection timed out." }
1147+ )
1148+ except requests .exceptions .ConnectionError :
1149+ # Other connection error (ssl, etc) - does not make sense to
1150+ # try call server again
1151+ new_response = RestApiResponse (
1152+ None ,
1153+ {"detail" : "Unable to connect the server. Connection error" }
1154+ )
1155+ break
10281156
1029- except ConnectionRefusedError :
1030- new_response = RestApiResponse (
1031- None ,
1032- {"detail" : "Unable to connect the server. Connection refused" }
1033- )
1034- except requests .exceptions .ConnectionError :
1035- new_response = RestApiResponse (
1036- None ,
1037- {"detail" : "Unable to connect the server. Connection error" }
1038- )
1039- else :
1040- content_type = response .headers .get ("Content-Type" )
1041- if content_type == "application/json" :
1042- try :
1043- new_response = RestApiResponse (response )
1044- except JSONDecodeError :
1045- new_response = RestApiResponse (
1046- None ,
1047- {
1048- "detail" : "The response is not a JSON: {}" .format (
1049- response .text )
1050- }
1051- )
1157+ time .sleep (0.1 )
10521158
1053- elif content_type in ( "image/jpeg" , "image/png" ) :
1054- new_response = RestApiResponse ( response )
1159+ if new_response is not None :
1160+ return new_response
10551161
1056- else :
1162+ content_type = response .headers .get ("Content-Type" )
1163+ if content_type == "application/json" :
1164+ try :
10571165 new_response = RestApiResponse (response )
1166+ except JSONDecodeError :
1167+ new_response = RestApiResponse (
1168+ None ,
1169+ {
1170+ "detail" : "The response is not a JSON: {}" .format (
1171+ response .text )
1172+ }
1173+ )
1174+
1175+ else :
1176+ new_response = RestApiResponse (response )
10581177
10591178 self .log .debug ("Response {}" .format (str (new_response )))
10601179 return new_response
0 commit comments