Skip to content

Commit 9b41a80

Browse files
committed
refactor: reduce _resolve function complexity
1 parent a9c96f2 commit 9b41a80

File tree

2 files changed

+74
-25
lines changed

2 files changed

+74
-25
lines changed

providers/openfeature-provider-ofrep/src/openfeature/contrib/provider/ofrep/__init__.py

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import re
2+
from datetime import datetime, timedelta, timezone
3+
from email.utils import parsedate_to_datetime
14
from typing import Any, Dict, List, Optional, Union
25
from 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

138162
def _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)

providers/openfeature-provider-ofrep/tests/test_provider.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from openfeature.evaluation_context import EvaluationContext
55
from openfeature.exception import (
66
FlagNotFoundError,
7+
GeneralError,
78
InvalidContextError,
89
ParseError,
910
)
@@ -88,7 +89,7 @@ def match_request_json(request):
8889
"metadata": {},
8990
"value": True,
9091
},
91-
additional_matcher=match_request_json
92+
additional_matcher=match_request_json,
9293
)
9394

9495
context = EvaluationContext("1", {"foo": "bar"})
@@ -101,3 +102,18 @@ def match_request_json(request):
101102
reason=Reason.TARGETING_MATCH,
102103
variant="true",
103104
)
105+
106+
107+
def test_provider_retry_after_shortcircuit_resolution(ofrep_provider, requests_mock):
108+
requests_mock.post(
109+
"http://localhost:8080/ofrep/v1/evaluate/flags/flag_key",
110+
status_code=429,
111+
headers={"Retry-After": "1"},
112+
)
113+
114+
with pytest.raises(GeneralError, match="Rate limited, retry after: 1"):
115+
ofrep_provider.resolve_boolean_details("flag_key", False)
116+
with pytest.raises(
117+
GeneralError, match="OFREP evaluation paused due to TooManyRequests"
118+
):
119+
ofrep_provider.resolve_boolean_details("flag_key", False)

0 commit comments

Comments
 (0)