|
1 | | -from typing import List, Optional, Union |
| 1 | +from typing import Any, Dict, List, Optional, Union |
| 2 | +from urllib.parse import urljoin |
| 3 | + |
| 4 | +import requests |
| 5 | +from requests.exceptions import JSONDecodeError |
2 | 6 |
|
3 | 7 | from openfeature.evaluation_context import EvaluationContext |
4 | | -from openfeature.flag_evaluation import FlagResolutionDetails |
| 8 | +from openfeature.exception import ( |
| 9 | + ErrorCode, |
| 10 | + FlagNotFoundError, |
| 11 | + GeneralError, |
| 12 | + InvalidContextError, |
| 13 | + OpenFeatureError, |
| 14 | + ParseError, |
| 15 | + TargetingKeyMissingError, |
| 16 | +) |
| 17 | +from openfeature.flag_evaluation import FlagResolutionDetails, Reason |
5 | 18 | from openfeature.hook import Hook |
6 | 19 | from openfeature.provider import AbstractProvider, Metadata |
7 | 20 |
|
| 21 | +__all__ = ["OFREPProvider"] |
| 22 | + |
8 | 23 |
|
9 | 24 | class OFREPProvider(AbstractProvider): |
| 25 | + def __init__( |
| 26 | + self, |
| 27 | + base_url: str, |
| 28 | + *, |
| 29 | + headers: Optional[Dict[str, str]] = None, |
| 30 | + timeout: float = 5.0, |
| 31 | + ): |
| 32 | + self.base_url = base_url |
| 33 | + self.headers = headers |
| 34 | + self.timeout = timeout |
| 35 | + self.session = requests.Session() |
| 36 | + if headers: |
| 37 | + self.session.headers.update(headers) |
| 38 | + |
10 | 39 | def get_metadata(self) -> Metadata: |
11 | 40 | return Metadata(name="OpenFeature Remote Evaluation Protocol Provider") |
12 | 41 |
|
13 | 42 | def get_provider_hooks(self) -> List[Hook]: |
14 | 43 | return [] |
15 | 44 |
|
16 | | - def resolve_boolean_details( # type: ignore[empty-body] |
| 45 | + def resolve_boolean_details( |
17 | 46 | self, |
18 | 47 | flag_key: str, |
19 | 48 | default_value: bool, |
20 | 49 | evaluation_context: Optional[EvaluationContext] = None, |
21 | | - ) -> FlagResolutionDetails[bool]: ... |
| 50 | + ) -> FlagResolutionDetails[bool]: |
| 51 | + return self._resolve(flag_key, default_value, evaluation_context) |
22 | 52 |
|
23 | | - def resolve_string_details( # type: ignore[empty-body] |
| 53 | + def resolve_string_details( |
24 | 54 | self, |
25 | 55 | flag_key: str, |
26 | 56 | default_value: str, |
27 | 57 | evaluation_context: Optional[EvaluationContext] = None, |
28 | | - ) -> FlagResolutionDetails[str]: ... |
| 58 | + ) -> FlagResolutionDetails[str]: |
| 59 | + return self._resolve(flag_key, default_value, evaluation_context) |
29 | 60 |
|
30 | | - def resolve_integer_details( # type: ignore[empty-body] |
| 61 | + def resolve_integer_details( |
31 | 62 | self, |
32 | 63 | flag_key: str, |
33 | 64 | default_value: int, |
34 | 65 | evaluation_context: Optional[EvaluationContext] = None, |
35 | | - ) -> FlagResolutionDetails[int]: ... |
| 66 | + ) -> FlagResolutionDetails[int]: |
| 67 | + return self._resolve(flag_key, default_value, evaluation_context) |
36 | 68 |
|
37 | | - def resolve_float_details( # type: ignore[empty-body] |
| 69 | + def resolve_float_details( |
38 | 70 | self, |
39 | 71 | flag_key: str, |
40 | 72 | default_value: float, |
41 | 73 | evaluation_context: Optional[EvaluationContext] = None, |
42 | | - ) -> FlagResolutionDetails[float]: ... |
| 74 | + ) -> FlagResolutionDetails[float]: |
| 75 | + return self._resolve(flag_key, default_value, evaluation_context) |
43 | 76 |
|
44 | | - def resolve_object_details( # type: ignore[empty-body] |
| 77 | + def resolve_object_details( |
45 | 78 | self, |
46 | 79 | flag_key: str, |
47 | 80 | default_value: Union[dict, list], |
48 | 81 | evaluation_context: Optional[EvaluationContext] = None, |
49 | | - ) -> FlagResolutionDetails[Union[dict, list]]: ... |
| 82 | + ) -> FlagResolutionDetails[Union[dict, list]]: |
| 83 | + return self._resolve(flag_key, default_value, evaluation_context) |
| 84 | + |
| 85 | + def _resolve( |
| 86 | + self, |
| 87 | + flag_key: str, |
| 88 | + default_value: Union[bool, str, int, float, dict, list], |
| 89 | + evaluation_context: Optional[EvaluationContext] = None, |
| 90 | + ) -> FlagResolutionDetails[Any]: |
| 91 | + try: |
| 92 | + response = self.session.post( |
| 93 | + urljoin(self.base_url, f"/ofrep/v1/evaluate/flags/{flag_key}"), |
| 94 | + json=_build_request_data(evaluation_context), |
| 95 | + timeout=self.timeout, |
| 96 | + ) |
| 97 | + response.raise_for_status() |
| 98 | + |
| 99 | + 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 |
| 124 | + |
| 125 | + try: |
| 126 | + data = response.json() |
| 127 | + except JSONDecodeError as e: |
| 128 | + raise ParseError(str(e)) from e |
| 129 | + |
| 130 | + return FlagResolutionDetails( |
| 131 | + value=data["value"], |
| 132 | + reason=Reason[data["reason"]], |
| 133 | + variant=data["variant"], |
| 134 | + flag_metadata=data["metadata"], |
| 135 | + ) |
| 136 | + |
| 137 | + |
| 138 | +def _build_request_data( |
| 139 | + evaluation_context: Optional[EvaluationContext], |
| 140 | +) -> Dict[str, Any]: |
| 141 | + data: Dict[str, Any] = {} |
| 142 | + if evaluation_context: |
| 143 | + data["context"] = {} |
| 144 | + if evaluation_context.targeting_key: |
| 145 | + data["context"]["targetingKey"] = evaluation_context.targeting_key |
| 146 | + data["context"].update(evaluation_context.attributes) |
| 147 | + return data |
0 commit comments