1+ import re
2+ from datetime import datetime , timedelta , timezone
3+ from email .utils import parsedate_to_datetime
14from typing import Any , Dict , List , Optional , Union
25from urllib .parse import urljoin
36
@@ -32,7 +35,9 @@ def __init__(
3235 self .base_url = base_url
3336 self .headers = headers
3437 self .timeout = timeout
38+ self .retry_after : Optional [datetime ] = None
3539 self .session = requests .Session ()
40+ self .session .headers ["User-Agent" ] = "OpenFeature/1.0.0"
3641 if headers :
3742 self .session .headers .update (headers )
3843
@@ -88,6 +93,14 @@ def _resolve(
8893 default_value : Union [bool , str , int , float , dict , list ],
8994 evaluation_context : Optional [EvaluationContext ] = None ,
9095 ) -> FlagResolutionDetails [Any ]:
96+ now = datetime .now (timezone .utc )
97+ if self .retry_after and now <= self .retry_after :
98+ raise GeneralError (
99+ f"OFREP evaluation paused due to TooManyRequests until { self .retry_after } "
100+ )
101+ elif self .retry_after :
102+ self .retry_after = None
103+
91104 try :
92105 response = self .session .post (
93106 urljoin (self .base_url , f"/ofrep/v1/evaluate/flags/{ flag_key } " ),
@@ -97,30 +110,7 @@ def _resolve(
97110 response .raise_for_status ()
98111
99112 except requests .RequestException as e :
100- if e .response is None :
101- raise GeneralError (str (e )) from e
102-
103- try :
104- data = e .response .json ()
105- except JSONDecodeError :
106- raise ParseError (str (e )) from e
107-
108- if e .response .status_code == 404 :
109- raise FlagNotFoundError (data ["errorDetails" ]) from e
110-
111- error_code = ErrorCode (data ["errorCode" ])
112- error_details = data ["errorDetails" ]
113-
114- if error_code == ErrorCode .PARSE_ERROR :
115- raise ParseError (error_details ) from e
116- if error_code == ErrorCode .TARGETING_KEY_MISSING :
117- raise TargetingKeyMissingError (error_details ) from e
118- if error_code == ErrorCode .INVALID_CONTEXT :
119- raise InvalidContextError (error_details ) from e
120- if error_code == ErrorCode .GENERAL :
121- raise GeneralError (error_details ) from e
122-
123- raise OpenFeatureError (error_code , error_details ) from e
113+ self ._handle_error (e )
124114
125115 try :
126116 data = response .json ()
@@ -134,6 +124,40 @@ def _resolve(
134124 flag_metadata = data ["metadata" ],
135125 )
136126
127+ def _handle_error (self , exception : requests .RequestException ) -> None :
128+ response = exception .response
129+ if response is None :
130+ raise GeneralError (str (exception )) from exception
131+
132+ if response .status_code == 429 :
133+ retry_after = response .headers .get ("Retry-After" )
134+ self .retry_after = _parse_retry_after (retry_after )
135+ raise GeneralError (
136+ f"Rate limited, retry after: { retry_after } "
137+ ) from exception
138+
139+ try :
140+ data = response .json ()
141+ except JSONDecodeError :
142+ raise ParseError (str (exception )) from exception
143+
144+ error_code = ErrorCode (data ["errorCode" ])
145+ error_details = data ["errorDetails" ]
146+
147+ if response .status_code == 404 :
148+ raise FlagNotFoundError (error_details ) from exception
149+
150+ if error_code == ErrorCode .PARSE_ERROR :
151+ raise ParseError (error_details ) from exception
152+ if error_code == ErrorCode .TARGETING_KEY_MISSING :
153+ raise TargetingKeyMissingError (error_details ) from exception
154+ if error_code == ErrorCode .INVALID_CONTEXT :
155+ raise InvalidContextError (error_details ) from exception
156+ if error_code == ErrorCode .GENERAL :
157+ raise GeneralError (error_details ) from exception
158+
159+ raise OpenFeatureError (error_code , error_details ) from exception
160+
137161
138162def _build_request_data (
139163 evaluation_context : Optional [EvaluationContext ],
@@ -145,3 +169,12 @@ def _build_request_data(
145169 data ["context" ]["targetingKey" ] = evaluation_context .targeting_key
146170 data ["context" ].update (evaluation_context .attributes )
147171 return data
172+
173+
174+ def _parse_retry_after (retry_after : Optional [str ]) -> Optional [datetime ]:
175+ if retry_after is None :
176+ return None
177+ if re .match (r"^\s*[0-9]+\s*$" , retry_after ):
178+ seconds = int (retry_after )
179+ return datetime .now (timezone .utc ) + timedelta (seconds = seconds )
180+ return parsedate_to_datetime (retry_after )
0 commit comments