Skip to content

Commit f3ade95

Browse files
committed
refactor: unleash flag evaluation
Signed-off-by: Kiki L Hakiem <[email protected]>
1 parent 7e52a49 commit f3ade95

File tree

4 files changed

+747
-810
lines changed

4 files changed

+747
-810
lines changed
Lines changed: 15 additions & 275 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
1-
import json
21
from typing import Any, Callable, List, Mapping, Optional, Sequence, Union
32

43
from UnleashClient import UnleashClient
54
from UnleashClient.events import BaseEvent, UnleashReadyEvent
65
from openfeature.evaluation_context import EvaluationContext
76
from openfeature.event import ProviderEvent
8-
from openfeature.exception import (
9-
ErrorCode,
10-
FlagNotFoundError,
11-
GeneralError,
12-
ParseError,
13-
TypeMismatchError,
14-
)
15-
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType, Reason
7+
from openfeature.exception import ErrorCode, GeneralError
8+
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType
169
from openfeature.hook import Hook
1710
from openfeature.provider import AbstractProvider, Metadata, ProviderStatus
18-
import requests
1911

2012
from .events import EventManager
13+
from .flag_evaluation import FlagEvaluator
2114
from .tracking import Tracker
2215

2316
__all__ = ["UnleashProvider"]
@@ -51,6 +44,7 @@ def __init__(
5144
}
5245
self._tracking_manager = Tracker(self)
5346
self._event_manager = EventManager(self)
47+
self._flag_evaluator = FlagEvaluator(self)
5448

5549
def initialize(
5650
self, evaluation_context: Optional[EvaluationContext] = None
@@ -176,138 +170,16 @@ def _build_unleash_context(
176170
context.update(evaluation_context.attributes)
177171
return context
178172

179-
def _resolve_variant_flag(
180-
self,
181-
flag_key: str,
182-
default_value: Any,
183-
value_converter: Callable[[Any], Any],
184-
evaluation_context: Optional[EvaluationContext] = None,
185-
) -> FlagResolutionDetails[Any]:
186-
"""Helper method to resolve variant-based flags.
187-
188-
Args:
189-
flag_key: The flag key to resolve
190-
default_value: The default value to return if flag is disabled
191-
value_converter: Function to convert payload value to desired type
192-
evaluation_context: Optional evaluation context
193-
194-
Returns:
195-
FlagResolutionDetails with the resolved value
196-
"""
197-
if not self.client:
198-
raise GeneralError("Provider not initialized. Call initialize() first.")
199-
200-
try:
201-
# Use get_variant to get the variant payload
202-
context = self._build_unleash_context(evaluation_context)
203-
variant = self.client.get_variant(flag_key, context=context)
204-
205-
# Check if the feature is enabled and has a payload
206-
if variant.get("enabled", False) and "payload" in variant:
207-
try:
208-
payload_value = variant["payload"].get("value", default_value)
209-
value = value_converter(payload_value)
210-
return FlagResolutionDetails(
211-
value=value,
212-
reason=(
213-
Reason.TARGETING_MATCH
214-
if value != default_value
215-
else Reason.DEFAULT
216-
),
217-
variant=variant.get("name"),
218-
error_code=None,
219-
error_message=None,
220-
flag_metadata={
221-
"source": "unleash",
222-
"enabled": variant.get("enabled", False),
223-
"variant_name": variant.get("name") or "",
224-
"app_name": self.app_name,
225-
},
226-
)
227-
except (ValueError, TypeError) as e:
228-
# If payload value can't be converted, raise TypeMismatchError
229-
raise TypeMismatchError(str(e))
230-
except ParseError:
231-
# Re-raise ParseError directly
232-
raise
233-
else:
234-
return FlagResolutionDetails(
235-
value=default_value,
236-
reason=Reason.DEFAULT,
237-
variant=None,
238-
error_code=None,
239-
error_message=None,
240-
flag_metadata={
241-
"source": "unleash",
242-
"enabled": variant.get("enabled", False),
243-
"variant_name": variant.get("name") or "",
244-
"app_name": self.app_name,
245-
},
246-
)
247-
except (
248-
FlagNotFoundError,
249-
TypeMismatchError,
250-
ParseError,
251-
GeneralError,
252-
):
253-
# Re-raise specific OpenFeature exceptions
254-
raise
255-
except requests.exceptions.HTTPError as e:
256-
if e.response.status_code == 404:
257-
raise FlagNotFoundError(f"Flag not found: {e}")
258-
else:
259-
raise GeneralError(f"HTTP error: {e}")
260-
except Exception as e:
261-
raise GeneralError(f"Unexpected error: {e}")
262-
263173
def resolve_boolean_details(
264174
self,
265175
flag_key: str,
266176
default_value: bool,
267177
evaluation_context: Optional[EvaluationContext] = None,
268178
) -> FlagResolutionDetails[bool]:
269179
"""Resolve boolean flag details."""
270-
if not self.client:
271-
raise GeneralError("Provider not initialized. Call initialize() first.")
272-
273-
try:
274-
275-
def fallback_func() -> bool:
276-
return default_value
277-
278-
context = self._build_unleash_context(evaluation_context)
279-
value = self.client.is_enabled(
280-
flag_key, context=context, fallback_function=fallback_func
281-
)
282-
return FlagResolutionDetails(
283-
value=value,
284-
reason=(
285-
Reason.TARGETING_MATCH if value != default_value else Reason.DEFAULT
286-
),
287-
variant=None,
288-
error_code=None,
289-
error_message=None,
290-
flag_metadata={
291-
"source": "unleash",
292-
"enabled": value,
293-
"app_name": self.app_name,
294-
},
295-
)
296-
except (
297-
FlagNotFoundError,
298-
TypeMismatchError,
299-
ParseError,
300-
GeneralError,
301-
):
302-
# Re-raise specific OpenFeature exceptions
303-
raise
304-
except requests.exceptions.HTTPError as e:
305-
if e.response.status_code == 404:
306-
raise FlagNotFoundError(f"Flag not found: {e}")
307-
else:
308-
raise GeneralError(f"HTTP error: {e}")
309-
except Exception as e:
310-
raise GeneralError(f"Unexpected error: {e}")
180+
return self._flag_evaluator.resolve_boolean_details(
181+
flag_key, default_value, evaluation_context
182+
)
311183

312184
def resolve_string_details(
313185
self,
@@ -316,8 +188,8 @@ def resolve_string_details(
316188
evaluation_context: Optional[EvaluationContext] = None,
317189
) -> FlagResolutionDetails[str]:
318190
"""Resolve string flag details."""
319-
return self._resolve_variant_flag(
320-
flag_key, default_value, lambda payload_value: payload_value
191+
return self._flag_evaluator.resolve_string_details(
192+
flag_key, default_value, evaluation_context
321193
)
322194

323195
def resolve_integer_details(
@@ -327,8 +199,8 @@ def resolve_integer_details(
327199
evaluation_context: Optional[EvaluationContext] = None,
328200
) -> FlagResolutionDetails[int]:
329201
"""Resolve integer flag details."""
330-
return self._resolve_variant_flag(
331-
flag_key, default_value, lambda payload_value: int(payload_value)
202+
return self._flag_evaluator.resolve_integer_details(
203+
flag_key, default_value, evaluation_context
332204
)
333205

334206
def resolve_float_details(
@@ -338,8 +210,8 @@ def resolve_float_details(
338210
evaluation_context: Optional[EvaluationContext] = None,
339211
) -> FlagResolutionDetails[float]:
340212
"""Resolve float flag details."""
341-
return self._resolve_variant_flag(
342-
flag_key, default_value, lambda payload_value: float(payload_value)
213+
return self._flag_evaluator.resolve_float_details(
214+
flag_key, default_value, evaluation_context
343215
)
344216

345217
def resolve_object_details(
@@ -351,138 +223,6 @@ def resolve_object_details(
351223
Union[Sequence[FlagValueType], Mapping[str, FlagValueType]]
352224
]:
353225
"""Resolve object flag details."""
354-
355-
def object_converter(payload_value: Any) -> Union[dict, list]:
356-
if isinstance(payload_value, str):
357-
try:
358-
value = json.loads(payload_value)
359-
except json.JSONDecodeError as e:
360-
raise ParseError(str(e))
361-
else:
362-
value = payload_value
363-
364-
if isinstance(value, (dict, list)):
365-
return value
366-
else:
367-
raise ValueError("Payload value is not a valid object")
368-
369-
return self._resolve_variant_flag(flag_key, default_value, object_converter)
370-
371-
async def resolve_boolean_details_async(
372-
self,
373-
flag_key: str,
374-
default_value: bool,
375-
evaluation_context: Optional[EvaluationContext] = None,
376-
) -> FlagResolutionDetails[bool]:
377-
"""Resolve boolean flag details asynchronously."""
378-
if not self.client:
379-
raise GeneralError("Provider not initialized. Call initialize() first.")
380-
381-
try:
382-
383-
def fallback_func() -> bool:
384-
return default_value
385-
386-
context = self._build_unleash_context(evaluation_context)
387-
value = self.client.is_enabled(
388-
flag_key, context=context, fallback_function=fallback_func
389-
)
390-
return FlagResolutionDetails(
391-
value=value,
392-
reason=(
393-
Reason.TARGETING_MATCH if value != default_value else Reason.DEFAULT
394-
),
395-
variant=None,
396-
error_code=None,
397-
error_message=None,
398-
flag_metadata={
399-
"source": "unleash",
400-
"enabled": value,
401-
"app_name": self.app_name,
402-
},
403-
)
404-
except (
405-
FlagNotFoundError,
406-
TypeMismatchError,
407-
ParseError,
408-
GeneralError,
409-
):
410-
# Re-raise specific OpenFeature exceptions
411-
raise
412-
except requests.exceptions.HTTPError as e:
413-
if e.response.status_code == 404:
414-
raise FlagNotFoundError(f"Flag not found: {e}")
415-
else:
416-
raise GeneralError(f"HTTP error: {e}")
417-
except Exception as e:
418-
raise GeneralError(f"Unexpected error: {e}")
419-
420-
async def resolve_string_details_async(
421-
self,
422-
flag_key: str,
423-
default_value: str,
424-
evaluation_context: Optional[EvaluationContext] = None,
425-
) -> FlagResolutionDetails[str]:
426-
"""Resolve string flag details asynchronously."""
427-
return self._resolve_variant_flag(
428-
flag_key,
429-
default_value,
430-
lambda payload_value: payload_value,
431-
evaluation_context,
432-
)
433-
434-
async def resolve_integer_details_async(
435-
self,
436-
flag_key: str,
437-
default_value: int,
438-
evaluation_context: Optional[EvaluationContext] = None,
439-
) -> FlagResolutionDetails[int]:
440-
"""Resolve integer flag details asynchronously."""
441-
return self._resolve_variant_flag(
442-
flag_key,
443-
default_value,
444-
lambda payload_value: int(payload_value),
445-
evaluation_context,
446-
)
447-
448-
async def resolve_float_details_async(
449-
self,
450-
flag_key: str,
451-
default_value: float,
452-
evaluation_context: Optional[EvaluationContext] = None,
453-
) -> FlagResolutionDetails[float]:
454-
"""Resolve float flag details asynchronously."""
455-
return self._resolve_variant_flag(
456-
flag_key,
457-
default_value,
458-
lambda payload_value: float(payload_value),
459-
evaluation_context,
460-
)
461-
462-
async def resolve_object_details_async(
463-
self,
464-
flag_key: str,
465-
default_value: Union[Sequence[FlagValueType], Mapping[str, FlagValueType]],
466-
evaluation_context: Optional[EvaluationContext] = None,
467-
) -> FlagResolutionDetails[
468-
Union[Sequence[FlagValueType], Mapping[str, FlagValueType]]
469-
]:
470-
"""Resolve object flag details asynchronously."""
471-
472-
def object_converter(payload_value: Any) -> Union[dict, list]:
473-
if isinstance(payload_value, str):
474-
try:
475-
value = json.loads(payload_value)
476-
except json.JSONDecodeError as e:
477-
raise ParseError(str(e))
478-
else:
479-
value = payload_value
480-
481-
if isinstance(value, (dict, list)):
482-
return value
483-
else:
484-
raise ValueError("Payload value is not a valid object")
485-
486-
return self._resolve_variant_flag(
487-
flag_key, default_value, object_converter, evaluation_context
226+
return self._flag_evaluator.resolve_object_details(
227+
flag_key, default_value, evaluation_context
488228
)

0 commit comments

Comments
 (0)