1- import json
21from typing import Any , Callable , List , Mapping , Optional , Sequence , Union
32
43from UnleashClient import UnleashClient
54from UnleashClient .events import BaseEvent , UnleashReadyEvent
65from openfeature .evaluation_context import EvaluationContext
76from 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
169from openfeature .hook import Hook
1710from openfeature .provider import AbstractProvider , Metadata , ProviderStatus
18- import requests
1911
2012from .events import EventManager
13+ from .flag_evaluation import FlagEvaluator
2114from .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